يوم الثلاثاء الماضي كنت أراجع وثائق طبقة الوكلاء في Flashcards Open Source App، ووصلت إلى تلك اللحظة التي يعرفها أي مهندس أنظمة خلفية: كل شيء مرتب، مضبوط بالأنواع، واضح... لكنه مرهق قليلًا.
كان لدي 17 استدعاء منفصلًا لأدوات الوكلاء: list_cards وget_cards وsearch_cards وlist_due_cards وcreate_cards وupdate_cards وdelete_cards، ثم النمط نفسه مرة أخرى مع decks، بالإضافة إلى الوسوم، وإعدادات الجدولة، وسياق مساحة العمل، وسجل المراجعة. لم يكن هناك شيء معطوب. وهنا كانت المشكلة تحديدًا. كل شيء كان يعمل.
لكن الواجهة كانت تعج بذلك النوع من الضجيج الذي تمتلئ به واجهات LLM عادة. فالمهندس البشري يستطيع أن يمر على الوثائق مرة واحدة، ويبني عميلًا، ثم يكمل طريقه. أما النموذج فلا يملك هذا الترف. بل يضطر إلى إعادة تعلّم الواجهة نفسها من الأمثلة، والوصف، ورسائل الخطأ، مرة بعد أخرى. وإذا وزعت نية بسيطة على عدد كبير جدًا من الأدوات، فإن النموذج يدفع ثمن ذلك في كل مرة.
هذه هي طبقة الوكلاء التي تقف خلف flashcards-open-source-app.com، لذلك كان يهمني أن تبقى الواجهة العامة سهلة التعلّم، لا أن تكون صحيحة تقنيًا فحسب.
لذلك اختزلت كل ذلك في واجهة واحدة تعتمد لغة استعلام متخصصة تشبه SQL.
وليست PostgreSQL خامًا. لست جسورًا إلى هذا الحد.

كانت 17 أداة أكثر مما ينبغي
في النسخة القديمة كانت القراءة والكتابة موزعتين على عدة موارد منطقية:
- سياق مساحة العمل
- إعدادات الجدولة
- الوسوم
- البطاقات
- البطاقات المستحقة للمراجعة
- المجموعات
- سجل المراجعة
من زاوية الأنظمة الخلفية يبدو هذا مرتبًا. كل أداة تنفذ مهمة واحدة. كل مخطط واضح. وواجهة OpenAPI تبدو محترمة. إنه تصرف كلاسيكي من مهندس أنظمة خلفية.
أما من زاوية الوكيل، فهذا عبء إداري لا أكثر.
إذا أراد النموذج "بطاقات إنجليزية سريعة جرى تحديثها مؤخرًا"، فعليه أولًا أن يخمن هل هذا يندرج تحت list_cards أو search_cards أو غيرهما. ثم عليه أن يتذكر شكل الحمولة. ثم التقسيم إلى صفحات. ثم التصفية. ثم أداة ثانية إذا أراد تحديث صف واحد بعد قراءته.
يمكن جعل ذلك يعمل. وأنا فعلت ذلك بالفعل.
لكنني ببساطة لم أعد أستسيغه.
ماذا تغيّر
العقد العام الجديد اختُزل في أداة واحدة:
{
"sql": "SELECT * FROM cards WHERE tags OVERLAP ('english') AND effort_level IN ('fast', 'medium') ORDER BY updated_at DESC LIMIT 20 OFFSET 0"
}
والأداة نفسها تُستخدم للقراءة وعمليات الكتابة البسيطة.
{
"sql": "UPDATE cards SET back_text = 'Updated answer' WHERE card_id = '123e4567-e89b-42d3-a456-426614174000'"
}
هذه هي الفكرة كلها. صار على الوكلاء الداخليين والخارجيين أن يتعلموا واجهة واحدة بدل متحف صغير من أسماء الأدوات.
في السابق، كان على الوكيل أن يحدد أولًا أي أداة تناسب المهمة.
أما الآن، فيستطيع في معظم الحالات أن يبدأ من المهمة نفسها:
- اعرض لي البطاقات
- صفِّ حسب الوسم
- رتّب حسب وقت التحديث
- حدّث هذا الحقل
- احذف هذه الصفوف
وهذا أقرب بكثير إلى الطريقة التي تتحسس بها نماذج LLM الأنظمة فعلًا: تجرّب شيئًا، وتقرأ الخطأ، ثم تعيد المحاولة. ووجود لغة واحدة تشبه SQL يخدم هذه الحلقة أفضل بكثير من 17 أداة منفصلة.
لماذا اخترت SQL لا صيغة JSON أخرى
لم أختر SQL لأنني أردت تحويل المنتج إلى عميل لقاعدة بيانات.
اخترتها لأن معظم النماذج اللغوية الجيدة تملك أصلًا معرفة مسبقة واسعة بها. فالنموذج يعرف تقريبًا ما الذي يفترض أن تفعله SELECT وUPDATE وWHERE وORDER BY وLIMIT. وهذا يوفر قدرًا كبيرًا من الشرح.
إذا اخترعت لغة مخصصة بصيغة JSON، فعلى النموذج أن يتعلم أفعالي أنا، وبنيتي المتداخلة، وآليات التصفية، والحالات الطرفية، وحتى المزاج الذي كنت فيه عندما سميت الأشياء في ذلك الأسبوع. أما إذا أعطيته صيغة تشبه SQL، فغالبًا ما يقترب من الإجابة الصحيحة من المحاولة الأولى.
وحتى عندما يخطئ في الاستعلام، فإنه غالبًا يخطئ بطريقة مفيدة. وفي العادة يكون الخطأ واحدًا من هذه:
- اسم عمود خاطئ
- عبارة غير مدعومة
- غياب
ORDER BY - قيمة
LIMITأكبر مما ينبغي
وهذا أفضل بكثير من نمط الفشل التالي: "استدعى الأداة الخطأ، وبحمولة لا تناسبها، والآن يحتاج إلى إعادة قراءة نصف المواصفات."
أردت شيئًا يعرفه النموذج جزئيًا من البداية، ثم يصقله بالتجربة وإعادة المحاولة. وSQL ممتازة في ذلك.
الجزء المهم: هذا ليس PostgreSQL
الجزء الأهم في هذا التصميم هو ما الذي لا تفعله هذه الواجهة مطلقًا.
هي لا تنفّذ SQL خامًا على قاعدة البيانات الحقيقية.
بل تحلل السلسلة المكتوبة بصيغة تشبه SQL، وتتحقق منها وفق القواعد المنشورة، ثم تترجمها إلى العمليات الداخلية نفسها التي يستخدمها المنتج أصلًا. فهذه السلسلة هي اللغة العامة التي أقدمها في الواجهة، لا ممرًا مباشرًا إلى طبقة التخزين.
وهذا يسمح لي بالإبقاء على سلوك المجال الحقيقي في مكانه الصحيح:
- نطاق مساحة العمل يُضاف تلقائيًا على الخادم
- بعض حقول النظام يمكن قراءتها لكن لا يمكن الكتابة إليها
- بيانات المزامنة تبقى داخلية
- القواعد الأساسية للمجال تبقى داخل المعالجات الفعلية
- ويمكن لطبقة التخزين أن تتغير لاحقًا من دون كسر هذا العقد العام
هذا هو الخط الذي لم أرد تجاوزه. Flashcards Open Source App مبني ليعمل دون اتصال أولًا، ومع المزامنة في الحسبان منذ البداية. لا أريد للوكلاء أن يعبثوا بالجداول الخام ثم يتعاملوا مع ذلك كما لو كانت واجهة المنتج الرسمية.
لذلك فالعقد هنا واضح وصريح: صيغة تشبه SQL من الخارج، وسلوك آمن على مستوى المجال في الداخل.
قواعد اللغة جاءت أبسط مما توقعت
الإصدار الأول صغير عن قصد:
SELECTINSERTUPDATEDELETE
في البداية ظننت أنني سأحتفظ بقائمة أطول من الموارد المنطقية. ثم قلصت ذلك أيضًا.
وفي النهاية أبقيت الواجهة العامة قريبة من الكيانات الأساسية:
cardsdecksworkspacereview_events
هذا التغيير جعل كل شيء أنظف.
وبدلًا من نشر موارد إضافية مثل tags_summary أو due_cards أو صيغ جاهزة أخرى، أضفت بعض القوة إلى لغة الاستعلام نفسها. وكان أهم ما أضفته GROUP BY وبعض دوال التجميع.
وبذلك يستطيع النموذج أن يطلب الملخصات مباشرة، بدل أن يتعلم أداة أو موردًا منفصلًا لكل شكل من أشكال التلخيص كنت قد نشرته قبل شهر.
على سبيل المثال، أصبح هذا ممكنًا الآن:
SELECT tag, COUNT(*) AS card_count
FROM cards
GROUP BY tag
ORDER BY card_count DESC
LIMIT 20 OFFSET 0;
أو:
SELECT rating, COUNT(*) AS reviews
FROM review_events
GROUP BY rating
ORDER BY reviews DESC
LIMIT 10 OFFSET 0;
وهذا أبسط بكثير من الإبقاء على واجهات مخصصة لكل حاجة تقارير صغيرة.
القواعد ما تزال محدودة. ولست أحاول الإيحاء بأن هذا "Postgres كامل".
الأشياء التي لا أدعمها:
JOINCTE- الاستعلامات الفرعية
- تنفيذ عدة أوامر في طلب واحد
- دوال اعتباطية
- وصول مباشر إلى الجداول الداخلية
- كتابة مباشرة إلى حقول النظام المحمية
إذا بدا هذا مقيّدًا، فذلك لأنه مقيّد فعلًا. وهذا مقصود. فهذا بالضبط ما يجعل هذه الفكرة منضبطة وقابلة للصيانة.
بعض الاستعلامات من الواجهة الجديدة
قراءة البطاقات:
SELECT *
FROM cards
WHERE tags OVERLAP ('english', 'grammar')
AND effort_level IN ('fast', 'medium')
ORDER BY updated_at DESC
LIMIT 20 OFFSET 0;
قراءة البطاقات مع تجميعها حسب الوسم:
SELECT tag, COUNT(*) AS card_count
FROM cards
GROUP BY tag
ORDER BY card_count DESC
LIMIT 20 OFFSET 0;
إنشاء المجموعات:
INSERT INTO decks (name, effort_levels, tags)
VALUES
('Grammar', ('medium', 'long'), ('english', 'grammar'));
تحديث البطاقات:
UPDATE cards
SET back_text = 'Updated answer',
tags = ('english', 'verbs')
WHERE card_id = '123e4567-e89b-42d3-a456-426614174000';
حذف البطاقات:
DELETE FROM cards
WHERE card_id IN (
'123e4567-e89b-42d3-a456-426614174000',
'123e4567-e89b-42d3-a456-426614174001'
);
قراءة عدد أحداث المراجعة:
SELECT review_grade, COUNT(*) AS total_reviews
FROM review_events
GROUP BY review_grade
ORDER BY total_reviews DESC
LIMIT 10 OFFSET 0;
وهذا يغطي جزءًا كبيرًا مما كانت تؤديه قائمة الأدوات القديمة، من دون أن يجبر الوكيل على حفظ واجهة مستقلة لكل كيان ولكل نوع من الملخصات داخل التطبيق.
ومن الآثار الجانبية الجميلة هنا أن الوثائق نفسها أصبحت أقصر أيضًا. لم أعد بحاجة إلى شرح عشرين شكلًا مختلفًا من الحمولات. يكفي أن أعرض قواعد موجزة وعشرة أمثلة، ثم أترك النموذج يتعلم بالممارسة.
الجزء المزعج الذي أبقيت عليه رغم ذلك
هذا التصميم أبسط، لكنه ليس سحرًا.
أكبر سلبية صريحة هنا هي أنك ما إن تقول "يشبه SQL" حتى يبدأ الناس بمحاولة تطبيق عادات SQL الحقيقية. بعض هذه العادات سينجح، وبعضها لن ينجح. وعلى المنتج أن يكون واضحًا وصارمًا جدًا عندما يخرج الاستعلام عن القواعد المدعومة.
كما اتخذت مفاضلة سيكرهها المتشددون في قواعد البيانات: الإصدار الأول يستخدم LIMIT وOFFSET مباشرة داخل SQL بدل التقسيم إلى صفحات بالمؤشر (cursor).
أنا أعرف العيب. النتائج قد تتحرك بين الصفحات إذا تغيرت البيانات بين الطلبات. والتقسيم إلى صفحات بالمؤشر أكثر أمانًا.
ومع ذلك اخترت OFFSET لهذه الواجهة لأنه أسهل لمن يكتبون الوكلاء، وأسهل لعرضه في الأمثلة، وأسهل على النموذج أن يولده من دون معرفة إضافية بالبروتوكول. وفي واجهة برمجة التطبيقات هذه، أهتم أكثر بسهولة الاستخدام من أول محاولة، لا بسلوك مثالي لتقسيم الصفحات مع بيانات متحركة.
إذا بدأت هذه المفاضلة تسبب لي مشكلات فعلية، يمكنني تعديل اللغة المنشورة لاحقًا. أما الآن، فالبساطة هي الفائزة.
المكسب الحقيقي لم يكن في تقليل عدد الأدوات فقط
المكسب الأعمق هنا هو أن واجهة برمجة التطبيقات باتت تلائم الطريقة الطبيعية التي تستكشف بها النماذج اللغوية الأنظمة.
هي لا تريد جولة تعريفية على كل الأدوات. هي تريد مكانًا واحدًا لتجربة نية ما، والحصول على خطأ مفيد إذا كان التخمين خاطئًا.
ولهذا يبدو هذا الإصدار أفضل من السابق. ليس فقط لأنه أصغر، بل لأنه أسهل على النموذج أن يستنتجه.
في منتجات الوكلاء، ما يسهل على النموذج تخمينه يتفوق غالبًا على البنية الداخلية الأنيقة أكثر مما يتوقع كثيرون.
أين لن أفرض هذا النمط
لن أستخدم هذا النهج إذا كان المنتج يعتمد كثيرًا على عمليات خاصة بالمجال لا تأخذ شكل CRUD التقليدي.
إذا كانت العملية الحقيقية شيئًا مثل submit_review أو run_scheduler أو merge_learning_state، فإن التظاهر بأن كل شيء مجرد UPDATE يجعل واجهة برمجة التطبيقات أسوأ عادة. في هذه الحالات سأبقي الأوامر الصريحة للعمليات المعقدة، وأستخدم اللغة الشبيهة بـ SQL لطبقة القراءة العامة، وعمليات CRUD الأساسية، والتقارير الخفيفة.
هذا هو الجزء الذي تخطئ فيه فرق كثيرة. فهي إما أن تكشف طبقة التخزين الخام، وهذا متهور، أو أن تغلف كل عملية صغيرة جدًا داخل واجهة مخصصة، وهذا مرهق.
أما المنطقة العملية في الوسط فهي:
- لغة استعلام تشبه SQL للوصول الواسع إلى البيانات
- أوامر صريحة للأفعال الثقيلة على مستوى المجال
هذا التقسيم يبدو لي أكثر واقعية بكثير من أي من الطرفين.
لماذا أحب هذا الاتجاه
الخلاصة القصيرة بسيطة.
استبدلت فهرسًا واسعًا من الأدوات بلغة استعلام واحدة تفهم معظم نماذج LLM ملامحها أصلًا.
أما الصياغة الهندسية فهي أكثر جفافًا قليلًا فحسب:
أبقيت بنية النظام الخلفية الفعلية، وسلوك المزامنة، وقواعد المجال في أماكنها تمامًا، ثم وضعت فوقها عقدًا أنحف وأسهل في التعلّم.
هذا يبدو لي التقسيم الصحيح.
إذا كنت تبني واجهات برمجية للوكلاء، فلن أبدأ من سؤال "ما أنظف واجهة OpenAPI للبشر؟" بل من سؤال "ما الذي يستطيع النموذج استنتاجه بسرعة، بأقل قدر من الوثائق، وبأقل عدد من المحاولات؟"
أحيانًا لا تكون الإجابة واجهة جديدة.
وأحيانًا تكون لغة صغيرة.
إذا أردت رؤية المنتج نفسه فهو هنا: flashcards-open-source-app.com
وإذا أردت الكود، فالمشروع على GitHub هنا: github.com/kirill-markin/flashcards-open-source-app. وهو مشروعي المفتوح المصدر بترخيص MIT.




