---
title: تقسيم النصوص مع مراعاة التوكنز لتقنية RAG في TypeScript (2026)
description: 'تقسيم النصوص بناءً على التوكنز لتقنية RAG في TypeScript: التقسيم المتكرر باستخدام @langchain/textsplitters و js-tiktoken، بالإضافة إلى ضبط حجم الأجزاء والتداخل.'
url: https://nerdleveltech.com/ar/text-chunking-rag-typescript-tutorial
language: ar
date: '2026-06-28'
category: llm-integration
tags:
  - text chunking
  - rag
  - typescript
  - langchain
  - js-tiktoken
  - embeddings
  - llm
  - ai-ml
markdown_url: https://nerdleveltech.com/ar/text-chunking-rag-typescript-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/text-chunking-rag-typescript-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# تقسيم النصوص مع مراعاة التوكنز لتقنية RAG في TypeScript (2026)

لتقسيم النص لتقنيات RAG في TypeScript، قم بتقسيم المستندات بناءً على عدد التوكنز (tokens) بدلاً من عدد الحروف: استخدم `RecursiveCharacterTextSplitter` مع وظيفة حساب طول تعتمد على `js-tiktoken` بحيث يناسب كل جزء ميزانية نموذج التضمين (embedding model) الخاص بك، وأضف تداخلاً (overlap) بنسبة عشرة إلى عشرين بالمائة، واحترم بنية Markdown أو الكود قبل الرجوع إلى فواصل أصغر.

## ملخص

التقسيم (Chunking) هو الخطوة التي تحدد جودة RAG، ومن السهل اختيار الوحدة الخاطئة: التقسيم بناءً على الحروف بينما تفكر نماذج التضمين بالتوكنز. يبني هذا الدليل مقسماً واعياً بالتوكنز في **TypeScript** باستخدام **`@langchain/textsplitters` 1.0.1** و **`js-tiktoken` 1.0.21**. ستتعلم كيفية عد التوكنز بنفس الترميز الذي يستخدمه نموذج التضمين الخاص بك، وتقسيم المستند إلى أجزاء ذات حجم مناسب مع تداخل، وكتابة مقسم صغير من الصفر لفهم الخوارزمية، وتقسيم Markdown بناءً على بنيته الحقيقية، وإرفاق البيانات الميتا (metadata) للمصدر من أجل الاستشهادات، وقياس توزيع حجم التوكنز لأجزائك. تم تجميع كل كتل الكود باستخدام `tsc` وتشغيلها باستخدام `tsx` قبل النشر؛ والأرقام التي تراها هي مخرجات حقيقية. لا يلزم وجود مفتاح API ولا تحميل نموذج — فالتقسيم يحدث بالكامل على جهازك.

## ما ستتعلمه

- لماذا يؤدي تحديد حجم الأجزاء بالحروف إلى تدهور جودة الاسترجاع بصمت
- كيفية عد التوكنز بنفس الترميز الذي يستخدمه نموذج التضمين الخاص بك (`cl100k_base`)
- كيفية التقسيم بناءً على التوكنز باستخدام `RecursiveCharacterTextSplitter` ووظيفة حساب الطول
- كيف تعمل الخوارزمية التكرارية، من خلال بناء مقسم واعٍ بالتوكنز من الصفر
- كيفية اختيار حجم الجزء والتداخل لنوع الاستعلام الخاص بك
- كيفية تقسيم Markdown والكود بناءً على البنية بدلاً من نوافذ الحروف العشوائية
- كيفية إرفاق البيانات الميتا للمصدر حتى يتمكن الجزء المسترجع من الاستشهاد بمصدره
- كيفية قياس توزيع حجم الأجزاء واكتشاف الأجزاء التي تتجاوز الميزانية قبل عملية التضمين

## المتطلبات الأساسية

- **Node.js 20+** (تم الاختبار على Node 22.22.3). كل من `@langchain/textsplitters` و `@langchain/core` يتطلبان `engines.node >= 20`.[^textsplitters][^core]
- الإلمام بـ **TypeScript** و `async/await`.
- محرر نصوص ومحطة طرفية (terminal). **لا** تحتاج إلى مفتاح OpenAI، أو GPU، أو قاعدة بيانات متجهة (vector database) — يتوقف هذا الدليل عند النقطة التي تكون فيها الأجزاء جاهزة للتضمين.

أنشئ مشروعاً وثبّت كل تبعية لضمان إمكانية إعادة إنتاج البناء الخاص بك:

```bash
npm init -y
npm pkg set type=module
npm install @langchain/textsplitters@1.0.1 @langchain/core@1.2.1 js-tiktoken@1.0.21
npm install -D TypeScript@6.0.3 tsx@4.22.4 @types/node@26.0.1
```

تعتبر `@langchain/core` تبعية نظيرة (peer dependency) لـ `@langchain/textsplitters` — حيث يعيد المقسم كائنات `Document` الخاصة بـ `@langchain/core` من `createDocuments`، لذا قم بتثبيتها صراحةً بدلاً من الاعتماد على مدير الحزم لرفعها تلقائياً.[^textsplitters] أضف ملف `tsconfig.json`:

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "preserve",
    "moduleResolution": "bundler",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["node"]
  },
  "include": ["src"]
}
```

## لماذا يؤدي عد الحروف إلى كسر استرجاع RAG بصمت

يقوم نظام RAG باسترجاع فقرات من مستنداتك وتغذيتها للنموذج كسياق، لذا فإن الفقرات التي تخزنها هي الحد الأقصى لجودة الإجابة.[^robustrag] عملية التقسيم تنتج تلك الفقرات، ومن الأخطاء الشائعة والمكلفة قياس حجم الجزء بعدد الحروف.

تقرأ نماذج التضمين **التوكنز**، وليس الحروف. التوكن الواحد يعادل تقريباً أربعة حروف من النص الإنجليزي، لكن هذه النسبة هي قاعدة تقريبية وليست ثابتة — فالكود، وعلامات الترقيم، والنصوص غير الإنجليزية يتم تحويلها إلى توكنز بشكل مختلف تماماً.[^openaitokens] المستند النموذجي المستخدم في هذا الدليل يتكون من 2,736 حرفاً ولكن 511 توكن فقط، بنسبة حوالي 5.4 حرف لكل توكن بدلاً من 4. إذا قمت بضبط حجم الجزء على "256" وكنت تقصد بصمت 256 حرفاً، فأنت تخصص ميزانية تبلغ حوالي 50 توكن — أي حوالي خمس ما كنت تنويه — وسيكون الاسترجاع الخاص بك مليئاً بشظايا ضيقة ومفتقرة للسياق.

الحل هو جعل كل قياس يعتمد على التوكنز، باستخدام نفس المحلل (tokenizer) الذي يستخدمه نموذج التضمين الخاص بك. بقية هذا الدليل مبنية على هذا القرار الواحد.

## الخطوة 1 — عد التوكنز بنفس الطريقة التي يتبعها نموذج التضمين الخاص بك

`js-tiktoken` هو منفذ بلغة JavaScript الخالصة لمكتبة OpenAI المسماة `tiktoken`. وهو يشحن جداول ترميز زوج البايت (byte-pair-encoding) داخل الحزمة، لذا فهو يعد التوكنز بالكامل دون اتصال بالإنترنت — لا حاجة لطلب شبكة ولا تحميل نموذج.[^tiktoken] التفصيل المهم هو اختيار الترميز الصحيح. تستخدم عائلات النماذج المختلفة محللات مختلفة، وتختلف حدود التوكنز بينها:

| النموذج                  | الترميز (`js-tiktoken`) |
| ------------------------ | ----------------------- |
| `text-embedding-3-small` | `cl100k_base`           |
| `text-embedding-3-large` | `cl100k_base`           |
| `text-embedding-ada-002` | `cl100k_base`           |
| `gpt-4o`, `gpt-4o-mini`  | `o200k_base`            |
| `gpt-4`                  | `cl100k_base`           |

تم إنتاج التعيين أعلاه عن طريق استدعاء `getEncodingNameForModel` لكل نموذج. النتيجة العملية: يستخدم كل من `text-embedding-3-small` و `text-embedding-3-large` من OpenAI ترميز `cl100k_base` (كما يفعل النموذج الأقدم `text-embedding-ada-002`)، لذا فهذا هو الترميز الذي يجب أن تعد به عندما تخصص ميزانية الأجزاء لها.[^tiktoken][^cookbook] العد باستخدام `o200k_base` (ترميز دردشة GPT-4o) سيعطيك حدوداً مختلفة قليلاً وميزانية مضللة.

بدلاً من كتابة اسم الترميز بشكل ثابت، دع اسم النموذج يختاره. تقوم وظيفة `encodingForModel` بتحديد الترميز الصحيح لك. أنشئ ملف `src/tokens.ts`:

```TypeScript
import { encodingForModel, type TiktokenModel } from "js-tiktoken";

// Pick the encoding that matches YOUR embedding model.
// The text-embedding-3-* models all use cl100k_base.
export function tokenCounter(model: TiktokenModel = "text-embedding-3-small") {
  const enc = encodingForModel(model);
  return (text: string): number => enc.encode(text).length;
}
```

تعيد وظيفة `tokenCounter` دالة تحول أي سلسلة نصية إلى عدد التوكنز الخاص بها. هذه الدالة المرتجعة هي بالضبط الشكل الذي يحتاجه مقسم النصوص لوظيفة حساب الطول (length function)، وهو ما يجعل الخطوة التالية تعمل.

## الخطوة 2 — التقسيم بناءً على التوكنز، وليس الحروف

`RecursiveCharacterTextSplitter` هو المقسم الأساسي. يقوم بالمرور على قائمة مرتبة من الفواصل — افتراضياً `["\n\n", "\n", " ", ""]` — محاولاً التقسيم عند أكبر حد طبيعي أولاً (فواصل الفقرات)، ثم حدود أصغر تدريجياً (الأسطر، المسافات، وأخيراً الحروف الفردية) حتى يصبح كل جزء تحت حد الحجم. ثم يقوم بدمج الأجزاء المتجاورة مرة أخرى حتى الحد الأقصى ويضيف التداخل المكون.[^langchaindocs]

يتم قياس حد الحجم بواسطة `lengthFunction` الخاصة بالمقسم. القيمة الافتراضية لها هي `text.length` — أي الحروف. قم بتمرير عداد التوكنز من الخطوة 1 وسيصبح رقم `chunkSize` الآن يعني التوكنز. أنشئ ملف `src/chunk.ts`:

```TypeScript
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { tokenCounter } from "./tokens";

const countTokens = tokenCounter("text-embedding-3-small");

export function makeSplitter(chunkSize = 256, chunkOverlap = 32) {
  return new RecursiveCharacterTextSplitter({
    chunkSize,
    chunkOverlap,
    lengthFunction: countTokens, // <-- chunkSize now counts TOKENS
  });
}
```

لمعرفة سبب أهمية ذلك، قم بتشغيل نفس المستند عبر المقسم مرتين — مرة باستخدام وظيفة طول الحروف الافتراضية ومرة باستخدام الوعي بالتوكنز. مع المستند النموذجي المكون من 511 توكن و `chunkSize: 256`، فإن النتائج ليست متقاربة:

- **وضع الحروف** (`256` تعني حروفاً): **19 قطعة**، بأحجام توكنات `[8, 50, 33, 7, 46, 47, 17, 9, 49, 47, 19, 7, 54, 46, 9, 7, 47, 44, 7]`.
- **وضع التوكنات** (`256` تعني توكنات): **3 قطع**، بأحجام توكنات `[202, 228, 97]`.

نفس الرقم، نفس المستند، نتيجة مختلفة تماماً. وضع الحروف يحطم المستند إلى تسعة عشر شظية بمتوسط حوالي 30 توكن لكل منها — وهو جزء ضئيل من قطعة تضمين (embedding chunk) مفيدة — لأن 256 حرفاً تعادل حوالي 50 توكن فقط، وخطوط العناوين القصيرة تنفصل إلى قطع صغيرة خاصة بها. أما وضع التوكنات فينتج ثلاث قطع تملأ بالفعل الميزانية التي حددتها. الشظايا الناتجة عن وضع الحروف ستتحول كل منها إلى متجه (vector) غامض يطابق كل شيء ولا يجيب على شيء.

## الخطوة 3 — كيف يعمل المقسم، من الصفر

الاعتماد على مكتبة أمر جيد، لكن يجب أن تفهم الخوارزمية التي تشغلها، لأن الحالات الاستثنائية هي المكان الذي يحدث فيه خطأ في التقطيع (chunking). الفكرة الأساسية هي التعبئة الجشعة (greedy packing): تجميع وحدات صغيرة من النص حتى يؤدي إضافة الوحدة التالية إلى تجاوز الميزانية، ثم إصدار قطعة، ثم بدء القطعة التالية بذيل متداخل. أنشئ `src/scratch.ts`:

```TypeScript
export interface SplitOptions {
  chunkSize: number;
  chunkOverlap: number;
  countTokens: (text: string) => number;
}

export function splitByTokens(text: string, opts: SplitOptions): string[] {
  const { chunkSize, chunkOverlap, countTokens } = opts;
  // Break into atomic units: paragraphs, then sentences. Never split a unit further.
  const units = text
    .split(/\n\n+/)
    .flatMap((p) => p.match(/[^.!?]+[.!?]*\s*/g) ?? [p]);

  const chunks: string[] = [];
  let cur: string[] = [];

  for (const unit of units) {
    // Peek BEFORE committing: if this unit would overflow, close the chunk first.
    if (cur.length && countTokens([...cur, unit].join("")) > chunkSize) {
      chunks.push(cur.join("").trim());
      // Carry an overlapping tail into the next chunk.
      const tail: string[] = [];
      while (
        cur.length &&
        countTokens([cur[cur.length - 1], ...tail].join("")) <= chunkOverlap
      ) {
        tail.unshift(cur.pop() as string);
      }
      cur = tail;
    }
    cur.push(unit);
  }
  if (cur.join("").trim()) chunks.push(cur.join("").trim());
  return chunks;
}
```

التفصيل المهم هو كلمة **نظرة خاطفة (peek)**. النسخة الساذجة تدفع الوحدة أولاً وتتحقق من الحجم بعد ذلك، مما يسمح للقطعة بتجاوز الميزانية بجملة كاملة — في الاختبار، أنتجت النسخة الساذجة قطعاً بحجم 270 و 272 توكن مقابل ميزانية 256. التحقق *قبل* الالتزام يبقي كل قطعة عند الحد المسموح به أو أقل منه. عند التشغيل على المستند النموذجي باستخدام `256/32`، تعيد هذه النسخة **3 قطع** بأحجام `[242, 239, 51]`، ولا يوجد أي منها فوق الميزانية.

هناك حالة واحدة لا تستطيع حتى نسخة "النظرة الخاطفة" إصلاحها: وحدة واحدة هي *بحد ذاتها* أكبر من الميزانية — جملة مكونة من 400 توكن، أو سطر طويل من الكود المضغوط (minified) بدون مسافات. لا يمكن للمعبئ الجشع إلا أن يصدرها بحجم زائد. وهذا هو بالضبط سبب كون `RecursiveCharacterTextSplitter` *متكرراً (recursive)*: عندما تكون الوحدة كبيرة جداً، فإنه ينتقل إلى الفاصل التالي ويقسم تلك الوحدة أكثر، مكرراً العملية وصولاً إلى الحروف الفردية إذا اضطر لذلك. للإنتاج، يفضل استخدام المكتبة؛ النسخة المبنية من الصفر موجودة هنا حتى تتوقف المكتبة عن كونها صندوقاً أسود.

## الخطوة 4 — اختيار حجم القطعة والتداخل

لا يوجد حجم مثالي عالمي، ولكن المقايضات مفهومة جيداً، ويمكنك التفكير فيها بدلاً من التخمين.[^chunking] القطع الصغيرة (حوالي 128–256 توكن) تجعل كل تضمين مركزاً بشكل حاد، مما يساعد في عمليات البحث عن الحقائق حيث تكون الإجابة جملة أو جملتين. القطع الأكبر (حوالي 256–512 توكن) تحافظ على سياق محيط أكبر، مما يساعد في الأسئلة التحليلية التي تحتاج إلى فقرة من الاستدلال. معظم خطوط المعالجة تستقر في مكان ما في نطاق 256–512 كنقطة بداية وتضبط من هناك باستخدام تقييم الاسترجاع.

بالنسبة للتداخل (overlap)، فإن نقطة البداية الشائعة هي عشرة إلى عشرين بالمائة من حجم القطعة — وهو ما يكفي لنقل جملة عبر الحدود دون تخزين نص شبه مكرر.[^chunking] الافتراضي `256/32` المستخدم في هذا الدليل هو تداخل بنسبة 12.5 بالمائة، وهو يقع بشكل مريح داخل هذا النطاق. التداخل ليس مجانياً: عند تداخل بنسبة عشرين بالمائة، فإن حوالي خمس النص الذي تخزنه يكون مكرراً، مما يؤدي إلى تضخم عدد المتجهات وفاتورة التضمين الخاصة بك. تداخل قليل جداً وقد تصبح حقيقة تتوزع على الحدود غير قابلة للاسترجاع؛ تداخل كبير جداً وسيستمر نفس الاستعلام في إرجاع قطع متجاورة شبه مكررة.

من السهل رؤية التداخل في المخرجات الحقيقية. مع `256/32`، تشترك نهاية القطعة 0 وبداية القطعة 1 في نفس العنوان:

```text
chunk 0 (202 tokens) ... ends: "...preserving enough context to stand alone.

## Tokens are the real unit, not characters"

chunk 1 (228 tokens) starts: "## Tokens are the real unit, not characters

Embedding models read tokens, not characters..."
```

يظهر العنوان `## Tokens are the real unit` في ذيل إحدى القطع ورأس القطعة التالية. إذا سأل مستخدم عن التوكنات، يمكن استرجاع أي من القطعتين وستظل تحمل العنوان الذي يحدد إطار القسم.

## الخطوة 5 — احترام هيكل المستند: Markdown والكود

التقسيم بناءً على فواصل `\n\n`/`\n`/مسافة خام يعامل المستند كتدفق مسطح من النص. يؤدي ذلك إلى فصل العنوان عن قسمه وتقطيع كتل الكود إلى نصفين. يقوم `RecursiveCharacterTextSplitter.fromLanguage` بتبديل قائمة الفواصل لتناسب تنسيقاً معيناً، بحيث يكسر المقسم عند الحدود الهيكلية أولاً.[^langchaindocs] بالنسبة لـ Markdown، تبدأ قائمة الفواصل بالعناوين وعلامات الكتل:

```TypeScript
import { RecursiveCharacterTextSplitter from "@langchain/textsplitters";
import { tokenCounter from "./tokens";

const countTokens = tokenCounter("text-embedding-3-small");

const mdSplitter = RecursiveCharacterTextSplitter.fromLanguage("markdown", {
  chunkSize: 256,
  chunkOverlap: 0,
  lengthFunction: countTokens,
});
```

فواصل Markdown، بالترتيب، هي العناوين من المستوى الثاني إلى السادس (`\n## ` … `\n###### `)، ونهاية كتلة كود مسيجة، والخطوط الأفقية (`***`، `---`، `___`)، ثم الفقرة المعتادة، والسطر، والمسافة، والحروف كبدائل أخيرة. (لاحظ أن المستوى الأول `# ` ليس في القائمة — تبدأ الفواصل من المستوى الثاني، لأن المستند عادةً ما يحتوي على عنوان واحد فقط.) عند التشغيل على المستند النموذجي باستخدام `256/0`، ينتج عن ذلك **3 قطع** بأحجام `[193, 221, 97]`، كل واحدة منها محاذية لعنوان قسم بدلاً من إزاحة حرف عشوائية.

تنطبق نفس الفكرة على الكود المصدري: يدعم `fromLanguage` العديد من اللغات، وبالنسبة للكود فإنه يقسم عند حدود الدوال والفئات قبل الأسطر، لذلك نادراً ما يتم قطع دالة إلى نصفين. عندما تستوعب محتوى مختلطاً، قم بتوجيه كل ملف إلى المقسم الذي يطابق نوعه — Markdown للمستندات، واللغة المطابقة للكود — بدلاً من تشغيل كل شيء من خلال مقسم عام واحد.

## الخطوة 6 — إرفاق البيانات الوصفية حتى تتمكن من الاستشهاد بالمصادر

تكون القطعة أكثر فائدة بكثير عندما تتذكر من أين أتت. يأخذ `createDocuments` مصفوفة من النصوص ومصفوفة موازية من البيانات الوصفية، ويعيد كائنات `Document` تحمل كلاً من نص القطعة وبياناتها الوصفية — بما في ذلك نطاق الأسطر الذي تمتد عليه القطعة في المستند الأصلي. قم بتوسيع `src/chunk.ts`:

```TypeScript
export async function chunkText(text: string, source: string) {
  const splitter = makeSplitter();
  const docs = await splitter.createDocuments([text], [{ source ]);
  return docs.map((d) => ({
    text: d.pageContent,
    tokens: countTokens(d.pageContent),
    metadata: d.metadata,
  }));
}
```

بالنسبة للقطعة الأولى، تبدو `metadata` كالتالي:

```json
{ "source": "rag-intro.md", "loc": { "lines": { "from": 1, "to": 9 } } }
```

حقل `source` هو أي شيء قمت بتمريره؛ ونطاق `loc.lines` يضاف بواسطة المقسم. احمل هذه البيانات الوصفية طوال الطريق إلى مخزن المتجهات الخاص بك، وعندما يتم استرجاع قطعة ما، يمكنك أن تظهر للمستخدم بالضبط أي ملف وأي أسطر استندت إليها الإجابة — الفرق بين "تقول الوثائق X" و "تقول الوثائق X (rag-intro.md، الأسطر 1-9)". هذا المصدر هو أيضاً ما يسمح لك بتصحيح الإجابات السيئة والعودة إلى المقطع الذي تسبب فيها.

## الخطوة 7 — قياس توزيع القطع الخاص بك

يجب ألا تشحن أبداً تكوين تقطيع لم تقم بقياسه. خطأ برمجي من سطر واحد في الفواصل الخاصة بك أو قطعة زائدة الحجم بشكل عشوائي سوف يتجاوز بهدوء حد إدخال نموذج التضمين الخاص بك أو يهدر التوكنات على شظايا شبه فارغة. أنشئ `src/measure.ts`:

```TypeScript
export function measure(
  chunks: string[],
  countTokens: (t: string) => number,
  budget: number
) {
  const sizes = chunks.map(countTokens);
  return {
    count: sizes.length,
    min: Math.min(...sizes),
    max: Math.max(...sizes),
    avg: Math.round(sizes.reduce((a, b) => a + b, 0) / sizes.length),
    overBudget: sizes.filter((s) => s > budget).length,
  ;
}
```

`overBudget` هو الرقم الأكثر أهمية: يجب أن يكون صفراً. هناك تفصيل دقيق يستحق المعرفة — يمكن أن تنحرف أحجام القطع توكناً أو اثنين *فوق* الميزانية حتى عندما يعمل المقسم بشكل صحيح، لأن تحويل النص إلى توكنات (tokenization) عند نقطة الربط يعتمد على السياق (قطعتان يتم ترميز كل منهما إلى N توكن يمكن أن يتم ترميزهما إلى N+1 عند دمجهما). في اختبار منفصل بميزانية 128 توكن، استقرت القطع عند أحجام مثل `[127, 128, 127, 129, 128, ...]`. الدفاع العملي هو ترك حوالي عشرة بالمائة من مساحة الأمان: إذا كان سقفك الصارم هو 512 توكن، فاجعل ميزانية القطعة 460-480، وليس 512.

## التحقق

قم بربط القطع معاً في `src/main.ts` وقم بتشغيله:

```TypeScript
import { RecursiveCharacterTextSplitter from "@langchain/textsplitters";
import { tokenCounter from "./tokens";
import { measure from "./measure";
import { doc from "./sample";

const countTokens = tokenCounter("text-embedding-3-small");

const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 256,
  chunkOverlap: 32,
  lengthFunction: countTokens,
);

const docs = await splitter.createDocuments([doc], [{ source: "rag-intro.md" ]);

for (const [i, dof docs.entries()) {
  console.log(
    `chunk ${i}: ${countTokens(d.pageContent)} tokens, ` +
      `lines ${d.metadata.loc.lines.from}-${d.metadata.loc.lines.to}`
  );
}
console.log(measure(docs.map((d) => d.pageContent), countTokens, 256));
```

ضع أي مستند Markdown متعدد الفقرات في `src/sample.ts` وقم بتصديره كـ `doc`، ثم قم بتشغيل:

```bash
npx tsx src/main.ts
```

مع المستند النموذجي المكون من 511 توكناً، تكون المخرجات:

```text
chunk 0: 202 tokens, lines 1-9
chunk 1: 228 tokens, lines 15-23
chunk 2: 97 tokens, lines 29-31
{ count: 3, min: 97, max: 228, avg: 176, overBudget: 0 }
```

ثلاثة أجزاء (chunks)، كل منها يقع بشكل مريح تحت ميزانية الـ 256 توكن، `overBudget: 0`، وكل جزء موسوم بالأسطر التي جاء منها. قم بتشغيل `npx tsc` بجانبه وسيجتاز المشروع فحص الأنواع (type-checks) بنجاح تحت وضع `strict`. لديك الآن أجزاء جاهزة للتحويل إلى تمثيلات رقمية (embeddings).

## الأخطاء الشائعة

**"أجزائي صغيرة جدًا — عشرات الشظايا بدلاً من حفنة قليلة."** لقد تركت دالة طول الأحرف الافتراضية في مكانها، لذا فإن `chunkSize: 256` تعني 256 حرفًا (\~50 توكن)، وليس 256 توكن. قم بتمرير `lengthFunction: countTokens` كما في الخطوة 2.

**"الجزء يزيد بضعة توكنات عن حدي الأقصى."** هذا متوقع: التقطيع (tokenization) عند الحدود يعتمد على السياق، لذا قد تنحرف الأحجام بمقدار توكن أو اثنين عن الميزانية. اضبط ميزانية الأجزاء الخاصة بك بنسبة 10% تقريبًا أقل من الحد الصارم للنموذج (الخطوة 7) بدلاً من ضبطها عليه تمامًا.

**"جزء واحد يزيد بمئات التوكنات عن الميزانية."** هناك وحدة ذرية واحدة — سطر طويل جدًا أو كتلة كود غير منقطعة — أكبر من الميزانية، ولا يستطيع المجمع الجشع (greedy packer) تقسيمها. استخدم `RecursiveCharacterTextSplitter` (وليس النسخة المبنية من الصفر)، والتي تتغلغل في فواصل أصغر وصولاً إلى الأحرف الفردية، وبالنسبة للكود استخدم `fromLanguage` حتى يتم التقسيم بناءً على بناء الجملة (syntax).

**"عدد التوكنات يبدو غير دقيق مقارنة بفاتورة نموذجي."** أنت تقوم بالعد باستخدام ترميز خاطئ. نماذج الـ embedding في عائلة `text-embedding-3-*` تستخدم `cl100k_base`؛ بينما عائلة دردشة GPT-4o تستخدم `o200k_base`. اترك `encodingForModel` يختارها بناءً على اسم النموذج (الخطوة 1).

**"العناوين وكتل الكود يتم تقسيمها في منتصف القسم."** أنت تستخدم الفواصل العامة. انتقل إلى `fromLanguage("markdown")` للمستندات واللغة المطابقة للكود (الخطوة 5) بحيث يتم تجربة الحدود الهيكلية أولاً.

## الخطوات التالية

لديك الآن مقسم أجزاء (chunker) مدرك للتوكنات: يقوم بالعد باستخدام ترميز نموذج الـ embedding الخاص بك، ويقسم بناءً على الهيكل، ويتداخل من أجل السياق، ويسم كل جزء بأسطر المصدر الخاصة به، ويقدم تقريرًا عن التوزيع يمكنك التحقق منه. تقسيم الأجزاء هو مرحلة الإدخال في عملية الاسترجاع (retrieval)، وضبطه بشكل صحيح هو أحد أرخص المكاسب النوعية في خط الإنتاج.

من هنا، هناك ثلاثة اتجاهات تستحق وقتك. أولاً، المحلل اللغوي (tokenizer) نفسه: نفس ترميزات `js-tiktoken` تدير ميزانية نافذة السياق، وهو ما تمت تغطيته في [عد التوكنات في TypeScript لنافذة السياق](https://nerdleveltech.com/count-tokens-TypeScript-context-window). ثانيًا، ما يحدث لهذه الأجزاء بعد ذلك — تحويلها لتمثيلات رقمية واسترجاعها — هو موضوع [بناء نظام RAG قوي](https://nerdleveltech.com/building-a-robust-rag-system-a-complete-implementation-guide). ثالثًا، بمجرد تفعيل الاسترجاع، يتم تغطية ضبط دقة الاستدعاء (recall) والملاءمة في [تقنيات تحسين RAG](https://nerdleveltech.com/rag-optimization-techniques-building-smarter-retrieval-augmented-systems)؛ حجم الجزء والتداخل الذي اخترته هنا هما أول المفاتيح التي ستقوم بضبطها.

[^textsplitters]: `@langchain/textsplitters` — npm package (peer dependency on `@langchain/core`, `engines.node >= 20`). <https://www.npmjs.com/package/@langchain/textsplitters>

[^core]: `@langchain/core` — npm package. <https://www.npmjs.com/package/@langchain/core>

[^robustrag]: Nerd Level Tech — "Building a Robust RAG System." /building-a-robust-rag-system-a-complete-implementation-guide

[^openaitokens]: OpenAI Help Center — "What are tokens and how to count them." <https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them>

[^tiktoken]: `js-tiktoken` — npm package (pure-JS port of OpenAI's tiktoken; encodings bundled). <https://www.npmjs.com/package/js-tiktoken>

[^cookbook]: OpenAI Cookbook — "How to count tokens with tiktoken" (third-generation embedding models use the `cl100k_base` encoding). <https://GitHub.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb>

[^langchaindocs]: LangChain.js — `@langchain/textsplitters` source for `RecursiveCharacterTextSplitter` (default separators, merge logic, and `getSeparatorsForLanguage`). <https://GitHub.com/langchain-ai/langchainjs/blob/main/libs/langchain-textsplitters/src/text_splitter.ts>

[^chunking]: Firecrawl — "Best Chunking Strategies for RAG (2026)" (chunk size and overlap ranges). <https://www.firecrawl.dev/blog/best-chunking-strategies-rag>
