
# لماذا استبدلت 17 أداة للوكلاء بـ DSL يشبه SQL

يوم الثلاثاء الماضي كنت أقرأ وثائق الوكلاء في Flashcards Open Source App، ووصلت إلى ذلك الشعور المألوف لمهندس الخلفية: كل شيء يبدو مرتبًا، مضبوط الأنواع، صريحًا، ومزعجًا قليلًا.

كان لدي 17 استدعاء أداة منفصلًا للوكلاء. `list_cards` و`get_cards` و`search_cards` و`list_due_cards` و`create_cards` و`update_cards` و`delete_cards`، ثم النمط نفسه مرة أخرى مع decks، بالإضافة إلى tags وإعدادات scheduler وسياق workspace وسجل المراجعات. لم يكن هناك شيء مكسور. وهذه كانت أكثر نقطة مزعجة. كل شيء كان يعمل.

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

هذه هي طبقة الوكلاء وراء [flashcards-open-source-app.com](https://flashcards-open-source-app.com/)، لذلك كان يهمني جدًا أن يكون السطح الخارجي سهل التعلم، لا مجرد صحيح تقنيًا.

لهذا ضغطت كل شيء داخل نقطة وصول واحدة بلغة DSL تشبه SQL.

وليست PostgreSQL خام. أنا لست شجاعًا إلى هذه الدرجة.

![نقطة وصول بلغة DSL تشبه SQL تستبدل 17 أداة منفصلة للوكلاء](/articles/sql-like-dsl-for-ai-agents.webp)

## 17 أداة كانت كثيرة جدًا

الإصدار القديم كان يحتوي على أدوات منفصلة للقراءة والكتابة عبر عدة موارد منطقية:

- سياق workspace
- إعدادات scheduler
- tags
- cards
- due cards
- decks
- review history

من جهة الخلفية، هذا يبدو مرتبًا. كل أداة تفعل شيئًا واحدًا. كل schema واضحة. وOpenAPI يبدو محترمًا. حركة كلاسيكية من مهندس backend.

لكن من جهة الوكيل، هذا مجرد أعمال ورقية.

إذا أراد النموذج "بطاقات إنجليزية سريعة تم تحديثها مؤخرًا"، فعليه أولًا أن يخمن هل هذا ينتمي إلى `list_cards` أو `search_cards` أو شيء آخر. ثم عليه أن يتذكر شكل payload. ثم pagination. ثم filtering. ثم أداة ثانية إذا أراد تحديث صف واحد بعد قراءته.

يمكن جعل ذلك يعمل. وأنا بالفعل جعلته يعمل.

لكنني توقفت عن حبه.

## ماذا تغير

العقد العام الجديد هو أداة واحدة فقط:

```json
{
  "sql": "SELECT * FROM cards WHERE tags OVERLAP ('english') AND effort_level IN ('fast', 'medium') ORDER BY updated_at DESC LIMIT 20 OFFSET 0"
}
```

نقطة الوصول نفسها للقراءة والكتابات البسيطة.

```json
{
  "sql": "UPDATE cards SET back_text = 'Updated answer' WHERE card_id = '123e4567-e89b-42d3-a456-426614174000'"
}
```

وهذه هي الفكرة كلها. الوكلاء الداخليّون والخارجيّون يتعلمون الآن سطحًا واحدًا بدل متحف صغير من أسماء الأدوات.

في السابق، كان على الوكيل أن يحدد أي أداة موجودة لهذا العمل.

الآن يمكنه غالبًا أن يبدأ من العمل نفسه:

- اعرض لي cards
- صفِّ حسب tag
- رتّب حسب وقت التحديث
- حدّث هذا الحقل
- احذف هذه الصفوف

وهذا أنسب بكثير للطريقة التي تجس بها نماذج LLM الأنظمة فعلًا. إنها تجرب شيئًا، تقرأ الخطأ، ثم تجرب مرة أخرى. لغة واحدة تشبه SQL تدير هذه الحلقة أفضل بكثير من 17 أداة منفصلة.

## لماذا اخترت SQL وليس كتلة JSON أخرى

لم أختر SQL لأنني أردت تحويل المنتج إلى عميل قاعدة بيانات.

اخترته لأن معظم نماذج LLM الجيدة أصلًا لديها معرفة مسبقة كبيرة به. النموذج يعرف تقريبًا ما الذي يفترض أن تفعله `SELECT` و`UPDATE` و`WHERE` و`ORDER BY` و`LIMIT`. وهذا يوفر كثيرًا من الشرح.

إذا اخترعت DSL مخصصًا بصيغة JSON، فعلى النموذج أن يتعلم أفعالي أنا، وبنيتي المتداخلة، وفلاتري، وحالات الحافة، والمزاج الذي كنت فيه حين سميت الأشياء ذلك الأسبوع. أما إذا أعطيته شكلًا يشبه SQL، فعادة يصل قريبًا من الإجابة الصحيحة من المحاولة الأولى.

وحتى عندما يخطئ في الاستعلام، فإنه غالبًا يخطئ بطريقة مفيدة. عادة تكون واحدة من هذه:

- اسم عمود خاطئ
- clause غير مدعومة
- غياب `ORDER BY`
- `LIMIT` كبير جدًا

وهذا أفضل بكثير من نمط الفشل التالي: "استدعى الأداة الخطأ، وبشكل payload خاطئ، والآن يحتاج إلى إعادة قراءة نصف المواصفات."

أردت شيئًا يستطيع النموذج أن يتحدثه نصف حديث مسبقًا، ثم ينظفه عبر المحاولة وإعادة المحاولة. وSQL ممتازة في ذلك.

## الجزء المهم: هذا ليس PostgreSQL

الجزء المهم في هذا التصميم هو ما الذي لا تفعله نقطة الوصول **أبدًا**.

هي لا تنفذ SQL خامًا ضد قاعدة البيانات الحقيقية.

بل تقوم بتحليل السلسلة الشبيهة بـ SQL، وتتحقق منها مقابل القواعد المنشورة، ثم تترجمها إلى العمليات الداخلية نفسها التي يستخدمها المنتج أصلًا. سلسلة SQL هنا هي DSL العامة. وليست نفقًا مباشرًا إلى التخزين.

وهذا يسمح لي بالإبقاء على سلوك المجال الحقيقي في مكانه الصحيح:

- نطاق workspace يُحقن على الخادم
- حقول النظام يمكن أن تكون قابلة للقراءة لكن غير قابلة للكتابة
- بيانات المزامنة تبقى داخلية
- invariants المجال تبقى في handlers الحقيقية
- التخزين يمكن أن يتغير لاحقًا من دون كسر العقد العام

هذا هو الخط الذي لم أرد تجاوزه. Flashcards Open Source App يعمل بأسلوب offline-first ومصمم مع المزامنة في الاعتبار. لا أريد للوكلاء أن يعدلوا الجداول الخام ويتصرفوا وكأن هذا هو API المنتج.

إذًا العقد صريح: شكل SQL من الخارج، وأمان المجال من الداخل.

## القواعد صارت أصغر مما توقعت

الإصدار الأول صغير عن قصد:

- `SELECT`
- `INSERT`
- `UPDATE`
- `DELETE`

في البداية ظننت أنني سأحتفظ بقائمة أطول من الموارد المنطقية. ثم قلصت ذلك أيضًا.

وفي النهاية أبقيت السطح العام قريبًا من الأسماء الأساسية:

- `cards`
- `decks`
- `workspace`
- `review_events`

هذا التغيير جعل كل شيء أنظف.

بدلًا من نشر موارد إضافية مثل `tags_summary` أو `due_cards` أو أي views مصاغة مسبقًا، أضفت قليلًا من قوة الاستعلام إلى اللغة نفسها. والأهم كان `GROUP BY` وبعض الدوال التجميعية.

وبذلك يستطيع النموذج أن يطلب الملخصات مباشرة بدل أن يتعلم أداة أو موردًا منفصلًا لكل شكل ملخص كنت قد عرضته الشهر الماضي.

على سبيل المثال، أصبح هذا ممكنًا الآن:

```sql
SELECT tag, COUNT(*) AS card_count
FROM cards
GROUP BY tag
ORDER BY card_count DESC
LIMIT 20 OFFSET 0;
```

أو:

```sql
SELECT rating, COUNT(*) AS reviews
FROM review_events
GROUP BY rating
ORDER BY reviews DESC
LIMIT 10 OFFSET 0;
```

وهذا أبسط بكثير من الإبقاء على endpoints مخصصة لكل حاجة reporting صغيرة.

القواعد ما تزال محدودة. أنا لا أحاول التظاهر بأن هذا "Postgres كامل".

الأشياء التي لا أدعمها:

- `JOIN`
- `CTE`
- subqueries
- تنفيذ عدة statements
- دوال عشوائية
- وصول مباشر إلى الجداول الداخلية
- كتابات مباشرة إلى حقول النظام المحمية

هذا يبدو مقيِّدًا لأنه فعلًا مقيِّد. وهذا جيد. هذا بالضبط ما يجعل هذه الفكرة صادقة وقابلة للصيانة.

## بعض الاستعلامات من السطح الجديد

قراءة cards:

```sql
SELECT *
FROM cards
WHERE tags OVERLAP ('english', 'grammar')
  AND effort_level IN ('fast', 'medium')
ORDER BY updated_at DESC
LIMIT 20 OFFSET 0;
```

قراءة cards مجمعة حسب tag:

```sql
SELECT tag, COUNT(*) AS card_count
FROM cards
GROUP BY tag
ORDER BY card_count DESC
LIMIT 20 OFFSET 0;
```

إنشاء decks:

```sql
INSERT INTO decks (name, effort_levels, tags)
VALUES
  ('Grammar', ('medium', 'long'), ('english', 'grammar'));
```

تحديث cards:

```sql
UPDATE cards
SET back_text = 'Updated answer',
    tags = ('english', 'verbs')
WHERE card_id = '123e4567-e89b-42d3-a456-426614174000';
```

حذف cards:

```sql
DELETE FROM cards
WHERE card_id IN (
  '123e4567-e89b-42d3-a456-426614174000',
  '123e4567-e89b-42d3-a456-426614174001'
);
```

قراءة عدد review events:

```sql
SELECT review_grade, COUNT(*) AS total_reviews
FROM review_events
GROUP BY review_grade
ORDER BY total_reviews DESC
LIMIT 10 OFFSET 0;
```

وهذا يغطي جزءًا كبيرًا مما كان يفعله فهرس الأدوات القديم، من دون أن يجبر الوكيل على حفظ endpoint منفصلة لكل اسم ولكل شكل تلخيص داخل التطبيق.

ومن الآثار الجانبية الجميلة هنا أن الوثائق نفسها تصبح أقصر. لم أعد بحاجة إلى شرح عشرين شكلًا مختلفًا من payload. يمكنني عرض grammar صغيرة وعشرة أمثلة وترك النموذج يتعلم بالممارسة.

## الجزء المزعج الذي أبقيته على أي حال

هذا التصميم أبسط، لكنه ليس سحرًا.

أكبر سلبية صريحة هنا هي أنه ما إن تقول "يشبه SQL" حتى يبدأ الناس بمحاولة استخدام عادات SQL الحقيقية. بعض هذه العادات سينجح. وبعضها لن ينجح. ويجب على المنتج أن يكون حاسمًا جدًا عندما يخرج الاستعلام عن القواعد المدعومة.

كما أنني اتخذت مقايضة أخرى سيكرهها عشاق قواعد البيانات: الإصدار الأول يستخدم `LIMIT` و`OFFSET` مباشرة داخل SQL بدل pagination المبنية على cursor.

أنا أعرف العيب. الصفحات يمكن أن تنزلق إذا تغيرت البيانات بين الطلبات. Pagination بالـ cursor أكثر أمانًا.

ومع ذلك اخترت `OFFSET` لهذا السطح لأنه أسهل لمن يكتبون الوكلاء، وأسهل للشرح في الأمثلة، وأسهل للنموذج كي يولده من دون معرفة إضافية بالبروتوكول. في هذا الـ API، أنا أهتم أكثر ببساطة الاستخدام الأول من سلوك pagination المثالي على بيانات متحركة.

إذا بدأت هذه المقايضة تؤلمني عمليًا، يمكنني تغيير اللغة المنشورة لاحقًا. أما الآن، فالبساطة تفوز.

## المكسب الحقيقي لم يكن تقليل عدد endpoints

المكسب الأعمق هنا هو أن الـ API أصبح يطابق الطريقة الطبيعية التي تستكشف بها نماذج اللغة الأنظمة.

هي لا تريد جولة متحفية عبر كل الأدوات. هي تريد مكانًا واحدًا لتجربة نية ما والحصول على خطأ مفيد إذا كان التخمين خاطئًا.

ولهذا يبدو هذا الإصدار أفضل من السابق. ليس فقط لأنه أصغر. بل لأنه أسهل في التخمين.

في منتجات الوكلاء، سهولة التخمين تتفوق على أناقة البنية الداخلية أكثر مما يتوقعه الناس غالبًا.

## أين لن أفرض هذا النمط

لن أستخدم هذا النهج إذا كان المنتج يعتمد بشدة على أفعال مجال معقدة ليست على شكل CRUD.

إذا كان الفعل الحقيقي شيئًا مثل `submit_review` أو `run_scheduler` أو `merge_learning_state`، فإن التظاهر بأن كل شيء مجرد `UPDATE` يجعل الـ API أسوأ عادة. في هذه الحالات سأُبقي الأوامر الصريحة للعمليات المعقدة، وأستخدم DSL الشبيهة بـ SQL لطبقة القراءة العامة وCRUD والتقارير الخفيفة.

هذا هو الجزء الذي تعكسه فرق كثيرة. إما أن تكشف التخزين الخام، وهذا متهور، أو أن تغلف كل عملية صغيرة جدًا داخل endpoint مخصصة، وهذا مرهق.

المنتصف المفيد هو:

- DSL تشبه SQL للوصول الواسع إلى البيانات
- أوامر صريحة للأفعال الثقيلة على مستوى المجال

هذا الانقسام يبدو لي أكثر واقعية بكثير من أي من الطرفين.

## لماذا أحب هذا الاتجاه

النسخة القصيرة بسيطة.

استبدلت فهرسًا عريضًا من الأدوات بلغة استعلام واحدة تستطيع معظم نماذج LLM أن تتحدثها نصف حديث مسبقًا.

أما النسخة الهندسية فهي أكثر مللًا بقليل فقط:

أبقيت بنية backend الحقيقية وسلوك المزامنة وinvariants في مكانها تمامًا، ثم وضعت فوقها عقدًا أرق وأسهل في التعلم.

هذا يبدو لي الانقسام الصحيح.

إذا كنت تبني APIs للوكلاء، فلن أبدأ من سؤال "ما أنظف سطح OpenAPI للبشر؟" بل من سؤال "ما الذي يستطيع النموذج استنتاجه بسرعة بأقل قدر من الوثائق وأقل عدد من المحاولات؟"

أحيانًا لا تكون الإجابة endpoint جديدة.

وأحيانًا تكون الإجابة لغة صغيرة.

إذا أردت رؤية المنتج نفسه فهو هنا: [flashcards-open-source-app.com](https://flashcards-open-source-app.com/)

وإذا أردت الكود، فالمشروع على GitHub هنا: [github.com/kirill-markin/flashcards-open-source-app](https://github.com/kirill-markin/flashcards-open-source-app). وهو مشروعي المفتوح المصدر بترخيص MIT.


---
*[View the styled HTML version of this page](https://kirill-markin.com/ar/maqalat/istabdalat-17-adat-lilwukala-bi-dsl-shabih-bisql)*

*Tip: Append `.md` to any URL on https://kirill-markin.com to get a clean Markdown version of that page.*