فهرست مقاله
در پایگاه دانش پاسخگوی تلفنی، پیش از ارزیابی پاسخ مدل باید بتوانیم نشان دهیم هر قطعه دقیقاً از کدام محدودهٔ سند آمده است. وجود تمام واژهها در مجموعهٔ قطعات کافی نیست؛ ممکن است شاهد موردنیاز میان دو قطعه شکسته باشد. در این آموزش، با سند فارسی ساختگی، قطعهبندی پنجرهای و آزمون محدودهها را پیاده میکنیم. هیچ مدل زبانی، embedding یا تماس واقعی اجرا نمیشود. نتیجه، قرارداد آزمونپذیر آمادهسازی متن است و دربارهٔ کیفیت پاسخ محصول یا ایمنی کار با تجهیزات ادعایی ندارد.
قرارداد قطعه و شاهد را پیش از بازیابی تعریف کنیم

متن آزمایش «راهنما: برق قطع؛ سپس کابل جدا شود.» است؛ این جمله صرفاً دادهٔ ساختگی برنامه است، نه دستورالعمل تعمیر یا ایمنی. طول آن در قرارداد ما ۳۴ نقطهٔ کد است. قطعه یک رکورد شامل شمارهٔ ترتیبی، آغاز، پایان و متن استخراجشده دارد. محدودهٔ [start,end) از صفر شروع میشود، آغاز را شامل میشود و پایان را شامل نمیشود. بنابراین طول قطعه باید دقیقاً برابر تفاضل دو مرز باشد.
شاهدِ انتخابی «برق قطع؛ سپس» در محدودهٔ [8,20) قرار دارد و ۱۲ نقطهٔ کد دارد. این شاهد کوچک، نمونهای برای آزمودن محصورشدن یک عبارت است؛ کاملبودن معنایی پاسخ را تعریف نمیکند. ورودی تابع علاوه بر مرزها، نقلقول موردانتظار را هم میگیرد. اگر برش سند با نقلقول یکسان نباشد، گزارش تولید نمیشود. برای سندی با عبارت تکراری، محل وقوع باید صریح باشد؛ انتخاب نخستین نتیجهٔ جستوجوی رشته جای این قرارداد را نمیگیرد.
برای جایگاه این مرحله در سامانه، راهنمای RAG و پایگاه دانش سازمانی را ببینید. آزمون این مقاله فقط پیششرط مکانیِ آمادهسازی شاهد را بررسی میکند؛ رتبهبندی، استناد پاسخ و مجوز دسترسی به سند آزمونهای جداگانه میخواهند.
واحد offset؛ نقطهٔ کد با بایت و توکن فرق دارد

طبق مستندات رسمی str پایتون، رشته دنبالهای تغییرناپذیر از نقاط کد Unicode است. پس len و برش این برنامه بر همین واحد کار میکنند. هیچ تبدیل واحدی به بایت، توکن مدل یا تعداد حروفِ دیدهشده در صفحه انجام نمیدهیم. نوشتن واژهٔ مبهم «کاراکتر» در قرارداد بین دو سرویس میتواند همین تفاوت را پنهان کند.
کاوشگر مستقل «میرود🙂é» در فایل کد بهصورت صریح، نیمفاصله و علامت ترکیبی دارد. خروجی اجرا ۹ نقطهٔ کد، ۲۰ بایت UTF-8 و ۱۰ واحد کد UTF-16 است. حرف لاتین و اعراب ترکیبی آن دو نقطهٔ کد جدا هستند؛ پنجرهٔ یکواحدی میتواند آنها را از هم جدا کند. Unicode HOWTO پایتون نیز تفاوت نمایش ظاهری و تعداد نقاط کد را توضیح میدهد. این قطعهساز، مرز واژه یا خوشهٔ نویسهای را حفظ نمیکند.
در مرجع Text Split مایکروسافت، خروجی offset برای UTF-8، UTF-16 و CodePoint تفکیک شده است. این منبع اهمیت نامگذاری واحد را روشن میکند؛ برنامهٔ ما پیادهسازی آن سرویس نیست. همچنین متن را پاکسازی یا نرمال نمیکنیم. اگر لایهٔ جستوجو از کلید نرمالشدهٔ فارسی استفاده کند، offset آن کلید را بدون نگاشت معتبر به سند اصلی منتقل نکنید.
پنجره، همپوشانی و پایان سند را بدون ابهام بسازیم

اندازهٔ پنجره را ۱۶ و همپوشانی را ۴ نقطهٔ کد میگذاریم. گام حرکت ۱۲ است. محدودههای موردانتظار، با محاسبهٔ دستی، [0,16)، [12,28) و [24,34) هستند. قطعهٔ آخر فقط ده واحد دارد. هنگامی که پایان یک قطعه به پایان سند رسید، حلقه متوقف میشود؛ ادامهدادن صرفاً برای رسیدن به شروع بعدی، دنبالهای زائد ایجاد میکند.
در این قرارداد آموزشی، اندازهٔ پنجره عدد صحیح دقیق بین یک و ۱۰۲۴ است؛ همپوشانی عدد صحیح نامنفی و کوچکتر از پنجره است. bool، float و رشتهٔ عددی پذیرفته نمیشوند. این محدودیت، گام مثبت را تضمین میکند. ورودی نیز رشتهٔ دقیق و حداکثر ۴۰۹۶ نقطهٔ کد است. سند خالی هیچ قطعهای ندارد؛ سند کوتاهتر از پنجره فقط یک قطعه دارد. اینها سقفهای قراردادی مثالاند، نه محدودیت عمومی پایگاه دانش.
راهنمای قطعهبندی اسناد مایکروسافت همپوشانی را یکی از انتخابهای حفظ زمینه معرفی میکند و وابستگی انتخابها به محتوا را مطرح میکند. اندازهها و قواعد این مثال مستقلاند؛ آنها را بهعنوان تنظیم پیشنهادی Azure یا مقدار بهینهٔ مدل معرفی نمیکنیم. برنامه فقط کتابخانهٔ استاندارد میخواهد و بدون شبکه اجرا میشود.
"""Offline, authored code-point chunking and evidence-span acceptance tests.
This is not a tokenizer, retrieval evaluation, production RAG service or an
implementation of Azure Text Split. No model, network or document download runs.
"""
import json
import platform
def chunk_text(text, window, overlap):
"""Keep the exact source str; return nonempty half-open code-point slices."""
if type(text) is not str:
raise TypeError('text must be an exact str')
if len(text) > 4096:
raise ValueError('teaching input limit is 4096 code points')
if type(window) is not int or type(overlap) is not int:
raise TypeError('window and overlap must be exact int, not bool')
if not 1 <= window <= 1024 or not 0 <= overlap < window:
raise ValueError('require 1<=window<=1024 and 0<=overlap<window')
chunks = []
start = 0
while start < len(text):
end = min(start + window, len(text))
chunks.append({'ordinal': len(chunks), 'start': start, 'end': end,
'text': text[start:end]})
if end == len(text):
break # Do not emit a redundant tail already covered by this chunk.
start = end - overlap
return chunks
def evidence_report(text, window, overlap, start, end, expected_quote):
"""Verify one declared occurrence; no find()-based occurrence guessing."""
chunks = chunk_text(text, window, overlap)
if type(start) is not int or type(end) is not int:
raise TypeError('span endpoints must be exact int, not bool')
if not 0 <= start < end <= len(text):
raise ValueError('require a nonempty half-open source span')
if type(expected_quote) is not str:
raise TypeError('expected quote must be an exact str')
if text[start:end] != expected_quote:
raise ValueError('expected quote does not match this source occurrence')
containing = [c['ordinal'] for c in chunks
if c['start'] <= start and end <= c['end']]
# This diagnostic is intentionally different from single-chunk containment.
union_complete = all(any(c['start'] <= p < c['end'] for c in chunks)
for p in range(start, end))
return {'span': [start, end], 'quote': expected_quote,
'containing_ordinals': containing,
'all_span_code_points_in_chunk_union': union_complete}
def fails(error, function, *args):
try:
function(*args)
except error:
return True
return False
def closed_form_ranges(n, window, overlap):
"""Independent count/stride oracle, not the production end-update loop."""
if n == 0:
return []
stride = window - overlap
count = 1 + (max(0, n - window) + stride - 1) // stride
return [(j * stride, min(j * stride + window, n)) for j in range(count)]
def run_example():
text = 'راهنما: برق قطع؛ سپس کابل جدا شود.'
chunks4 = chunk_text(text, 16, 4)
chunks8 = chunk_text(text, 16, 8)
quote = 'برق قطع؛ سپس'
evidence4 = evidence_report(text, 16, 4, 8, 20, quote)
evidence8 = evidence_report(text, 16, 8, 8, 20, quote)
intervals4 = [(c['start'], c['end']) for c in chunks4]
intervals8 = [(c['start'], c['end']) for c in chunks8]
covered = {p for c in chunks4 for p in range(c['start'], c['end'])}
no_overlap = chunk_text(text, 16, 0)
unicode_probe = 'می\u200cرود🙂e\u0301'
units = {'code_points': len(unicode_probe),
'utf8_bytes': len(unicode_probe.encode('utf-8')),
'utf16_code_units': len(unicode_probe.encode('utf-16-le')) // 2}
small_cases = 0
closed_form_agrees = True
for n in range(21):
for width in range(1, 7):
for shared in range(width):
actual = chunk_text('x' * n, width, shared)
closed_form_agrees &= [(c['start'], c['end']) for c in actual] == closed_form_ranges(n, width, shared)
small_cases += 1
span_cases = 0
set_oracle_agrees = True
for shared in (4, 8):
cs = chunk_text(text, 16, shared)
chunk_sets = [set(range(c['start'], c['end'])) for c in cs]
for left in range(len(text)):
for right in range(left + 1, len(text) + 1):
expected_ids = [j for j, positions in enumerate(chunk_sets)
if set(range(left, right)) <= positions]
actual = evidence_report(text, 16, shared, left, right, text[left:right])
set_oracle_agrees &= actual['containing_ordinals'] == expected_ids
span_cases += 1
repeated = 'خاموش خاموش'
checks = {
'hand_ranges_window16_overlap4': intervals4 == [(0, 16), (12, 28), (24, 34)],
'hand_chunk_texts_include_spaces_punctuation_and_short_tail':
[c['text'] for c in chunks4] == ['راهنما: برق قطع؛', 'قطع؛ سپس کابل جد', 'ل جدا شود.'],
'every_chunk_is_exact_original_half_open_slice': all(c['text'] == text[c['start']:c['end']] for c in chunks4),
'ordinals_lengths_and_source_boundaries_are_consistent':
[c['ordinal'] for c in chunks4] == [0, 1, 2] and
all(0 <= c['start'] < c['end'] <= 34 and len(c['text']) <= 16 for c in chunks4) and
chunks4[0]['start'] == 0 and chunks4[-1]['end'] == 34,
'all34_code_points_covered_with8_extra_stored_occurrences':
covered == set(range(34)) and sum(len(c['text']) for c in chunks4) == 42,
'union_coverage_does_not_imply_single_chunk_evidence':
evidence4['all_span_code_points_in_chunk_union'] is True and evidence4['containing_ordinals'] == [],
'overlap8_contains_this_same_span_and_stores58_occurrences':
intervals8 == [(0, 16), (8, 24), (16, 32), (24, 34)] and
evidence8['containing_ordinals'] == [1] and sum(len(c['text']) for c in chunks8) == 58,
'zero_overlap_is_lossless_union_without_repeated_occurrences':
''.join(c['text'] for c in no_overlap) == text and
[(c['start'], c['end']) for c in no_overlap] == [(0, 16), (16, 32), (32, 34)],
'whole_document_window_returns_one_identical_chunk':
chunk_text(text, 34, 0) == [{'ordinal': 0, 'start': 0, 'end': 34, 'text': text}],
'larger_window_and_exact_boundary_do_not_emit_redundant_tail':
len(chunk_text(text, 100, 3)) == 1 and len(chunk_text('x' * 16, 16, 4)) == 1,
'empty_document_returns_no_chunks_and_cannot_have_nonempty_evidence':
chunk_text('', 16, 4) == [] and fails(ValueError, evidence_report, '', 16, 4, 0, 0, ''),
'one_code_point_and_maximal_allowed_overlap_make_progress':
len(chunk_text('ی', 16, 15)) == 1 and
[(c['start'], c['end']) for c in chunk_text('abcd', 2, 1)] == [(0, 2), (1, 3), (2, 4)],
'unicode_units_differ_and_combining_mark_is_not_grapheme_safe':
units == {'code_points': 9, 'utf8_bytes': 20, 'utf16_code_units': 10} and
''.join(c['text'] for c in chunk_text(unicode_probe, 1, 0)) == unicode_probe and
unicode_probe[2] == '\u200c' and unicode_probe[7:8] == 'e' and unicode_probe[8:9] == '\u0301',
'repeated_quote_uses_declared_occurrence_and_rejects_mismatch':
evidence_report(repeated, 6, 0, 6, 11, 'خاموش')['containing_ordinals'] == [1] and
fails(ValueError, evidence_report, repeated, 6, 0, 1, 6, 'خاموش'),
'441_small_document_cases_match_closed_form_interval_oracle': bool(closed_form_agrees) and small_cases == 441,
'1190_nonempty_spans_match_independent_set_containment_oracle': bool(set_oracle_agrees) and span_cases == 1190,
'invalid_text_window_overlap_types_values_and_input_bound_rejected':
len(chunk_text('x' * 4096, 1024, 0)) == 4 and
all(fails(TypeError, chunk_text, bad, 16, 4) for bad in (None, b'x', 1, ['x'])) and
all(fails(TypeError, chunk_text, 'abc', w, o) for w, o in ((True, 0), (2.0, 0), (2, False), (2, 0.0))) and
all(fails(ValueError, chunk_text, 'abc', w, o) for w, o in ((0, 0), (-1, 0), (1025, 0), (2, -1), (2, 2), (2, 3))) and
fails(ValueError, chunk_text, 'x' * 4097, 16, 4),
'invalid_span_types_bounds_and_quote_type_rejected':
all(fails(TypeError, evidence_report, text, 16, 4, a, z, quote) for a, z in ((True, 20), (8, 20.0))) and
all(fails(ValueError, evidence_report, text, 16, 4, a, z, quote) for a, z in ((-1, 20), (8, 8), (20, 8), (8, 35))) and
fails(TypeError, evidence_report, text, 16, 4, 8, 20, b'quote'),
}
if len(checks) != 18 or not all(type(value) is bool and value for value in checks.values()):
raise AssertionError(checks)
return {'python': platform.python_version(),
'scope': 'Authored offline Unicode-code-point chunks and exact-source evidence spans; no RAG model, tokenizer, Azure call, retrieval or physical safety test.',
'fixture_text': text, 'source_code_points': len(text),
'offset_unit': 'Unicode code points; zero-based half-open [start,end)',
'chunks_overlap4': chunks4, 'evidence_overlap4': evidence4,
'chunks_overlap8': chunks8, 'evidence_overlap8': evidence8,
'stored_occurrences': {'overlap4': 42, 'overlap8': 58, 'unique_source': 34},
'unicode_probe_units': units,
'closed_form_fixture_cases': small_cases, 'set_oracle_span_cases': span_cases,
'checks': checks}
if __name__ == '__main__':
print(json.dumps(run_example(), ensure_ascii=False, indent=2))
پوشش سراسری سند با نگهداشتن شاهد در یک قطعه برابر نیست

برای همپوشانی چهار، تمام ۳۴ موقعیت سند در اجتماع محدودهها وجود دارند. مجموع طول قطعات ۴۲ است؛ بنابراین هشت رخداد ذخیرهشدهٔ اضافی داریم. این عدد، تعداد بایت فایل یا هزینهٔ توکن مدل نیست. باوجود پوشش سراسری، هیچ قطعهای شاهد [8,20) را کامل نگه نمیدارد: قطعهٔ اول زود تمام میشود و قطعهٔ دوم دیر شروع میشود.
با همپوشانی هشت، گام هشت میشود و چهار محدودهٔ [0,16)، [8,24)، [16,32) و [24,34) داریم. اکنون قطعهٔ شمارهٔ یک، با شمارهگذاری از صفر، شاهد انتخابشده را کامل در بر میگیرد. مجموع طولها ۵۸ و رخدادهای اضافی ۲۴ است. موفقیت این شاهد، تضمین همهٔ عبارتهای ۱۲واحدی، بهبود بازیابی یا انتخاب اندازهٔ بهینه نیست؛ فقط نتیجهٔ همین داده و قرارداد است.
مرجع مستقل اول، تعداد قطعات و آغازها را با فرمول بسته و گام ثابت حساب میکند و در ۴۴۱ ترکیب کوچک با حلقه مقایسه میشود. مرجع دوم، موقعیتهای هر قطعه را مجموعه میسازد و زیرمجموعهبودن شاهد را میسنجد؛ در برابر آزمون نامساوی مرزها، ۱۱۹۰ حالتِ شاهد بررسی میشود. این پیمایشها دو گروه از ۱۸ بررسی نامدار هستند، نه هزاران آزمون نامدار تازه. خروجی زیر اجرای Python 3.14.5 را ثبت میکند.
{
"python": "3.14.5",
"scope": "Authored offline Unicode-code-point chunks and exact-source evidence spans; no RAG model, tokenizer, Azure call, retrieval or physical safety test.",
"fixture_text": "راهنما: برق قطع؛ سپس کابل جدا شود.",
"source_code_points": 34,
"offset_unit": "Unicode code points; zero-based half-open [start,end)",
"chunks_overlap4": [
{
"ordinal": 0,
"start": 0,
"end": 16,
"text": "راهنما: برق قطع؛"
},
{
"ordinal": 1,
"start": 12,
"end": 28,
"text": "قطع؛ سپس کابل جد"
},
{
"ordinal": 2,
"start": 24,
"end": 34,
"text": "ل جدا شود."
}
],
"evidence_overlap4": {
"span": [
8,
20
],
"quote": "برق قطع؛ سپس",
"containing_ordinals": [],
"all_span_code_points_in_chunk_union": true
},
"chunks_overlap8": [
{
"ordinal": 0,
"start": 0,
"end": 16,
"text": "راهنما: برق قطع؛"
},
{
"ordinal": 1,
"start": 8,
"end": 24,
"text": "برق قطع؛ سپس کاب"
},
{
"ordinal": 2,
"start": 16,
"end": 32,
"text": " سپس کابل جدا شو"
},
{
"ordinal": 3,
"start": 24,
"end": 34,
"text": "ل جدا شود."
}
],
"evidence_overlap8": {
"span": [
8,
20
],
"quote": "برق قطع؛ سپس",
"containing_ordinals": [
1
],
"all_span_code_points_in_chunk_union": true
},
"stored_occurrences": {
"overlap4": 42,
"overlap8": 58,
"unique_source": 34
},
"unicode_probe_units": {
"code_points": 9,
"utf8_bytes": 20,
"utf16_code_units": 10
},
"closed_form_fixture_cases": 441,
"set_oracle_span_cases": 1190,
"checks": {
"hand_ranges_window16_overlap4": true,
"hand_chunk_texts_include_spaces_punctuation_and_short_tail": true,
"every_chunk_is_exact_original_half_open_slice": true,
"ordinals_lengths_and_source_boundaries_are_consistent": true,
"all34_code_points_covered_with8_extra_stored_occurrences": true,
"union_coverage_does_not_imply_single_chunk_evidence": true,
"overlap8_contains_this_same_span_and_stores58_occurrences": true,
"zero_overlap_is_lossless_union_without_repeated_occurrences": true,
"whole_document_window_returns_one_identical_chunk": true,
"larger_window_and_exact_boundary_do_not_emit_redundant_tail": true,
"empty_document_returns_no_chunks_and_cannot_have_nonempty_evidence": true,
"one_code_point_and_maximal_allowed_overlap_make_progress": true,
"unicode_units_differ_and_combining_mark_is_not_grapheme_safe": true,
"repeated_quote_uses_declared_occurrence_and_rejects_mismatch": true,
"441_small_document_cases_match_closed_form_interval_oracle": true,
"1190_nonempty_spans_match_independent_set_containment_oracle": true,
"invalid_text_window_overlap_types_values_and_input_bound_rejected": true,
"invalid_span_types_bounds_and_quote_type_rejected": true
}
}
معیار پذیرش و چهار تمرین با پاسخ

معیار پذیرشِ این مرحله، بازسازی دقیق برشها، قرارداد واحد روشن، پوشش موقعیتها و گزارش درست شاهد است. شناسهٔ سند، نسخهٔ همان متن و واحد offset باید همراه ارجاع نگهداری شوند؛ پس از ویرایش سند، مرز قدیمی را خودکار معتبر فرض نکنید. برای افزودن عنوان یا حذف فاصلهها نیز متنِ ارائهشده و نگاشت آن به منبع باید جدا ثبت شوند. کد حاضر این زیرساخت نسخهبندی یا سامانهٔ تولید پاسخ را پیاده نمیکند.
تمرین یک: آیا پوشش همهٔ موقعیتها برای قبولی شاهد کافی است؟ پاسخ: خیر؛ در حالت همپوشانی چهار، پوشش کامل است ولی فهرست قطعاتِ دربرگیرندهٔ شاهد خالی است. شرط پذیرش محلی، وجود دستکم یک محدودهٔ شامل کل شاهد است.
تمرین دو: چرا پایان شاهد ۲۰ است، نه ۱۹؟ پاسخ: آخرین موقعیت شاملشده ۱۹ است و قرارداد نیمهباز، مرز پایان را یک واحد بعد ثبت میکند. طول عبارت برابر ۲۰ منهای هشت، یعنی ۱۲ است.
تمرین سه: آیا پنجرهٔ یکواحدی مرز نویسهٔ نمایشی را حفظ میکند؟ پاسخ: خیر؛ در کاوشگر، e و علامت ترکیبی به دو برش میروند. برای مرزبندی نمایشی به سیاست و ابزار مناسب خوشهٔ نویسهای نیاز داریم.
تمرین چهار: شاهد طولانیتر از پنجره چه میشود؟ پاسخ: در این قطعهساز هیچ قطعهای آن را کامل نگه نمیدارد؛ پوشش اجتماع این مشکل را حل نمیکند. باید قرارداد اندازه، مرزبندی یا نحوهٔ گردآوری چند قطعه را جدا طراحی و ارزیابی کنیم، نه اینکه نتیجهٔ مدل را از این آزمایش استنتاج کنیم.
منابع در ۲۴ مهر ۱۴۰۵ خوانده شدهاند. این آموزش پایه و تألیفی است؛ تاریخ بهروزرسانی صفحات، تاریخ نخستین انتشار پژوهش نیست. مستندات Python 3.14.8 مرجع بوده و اجرای ثبتشده با 3.14.5 انجام شده است.
منابع و مطالعهٔ بیشتر
- Microsoft Learn؛ روشهای قطعهبندی اسناد و نقش همپوشانی در RAG
- Microsoft Learn Text Split؛ خروجی offset با واحدهای جداگانه
- Python 3.14 str؛ دنبالهٔ نقاط کد و برش رشته
- Python 3.14 Unicode HOWTO؛ علامت ترکیبی و تفاوت نمایش و طول
بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

