Transformers documentation

تحليل الاستجابة

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

تحليل الاستجابة

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

المشكلة في المخرجات المهيكلة، ومع ذلك، هي أن مخرجات نماذج اللغة الكبيرة ليست مهيكلة بطبيعتها. تقبل واجهات برمجة تطبيقات نماذج اللغة الكبيرة عادةً وتعيد قواميس رسائل، بمفاتيح مثل role وcontent وreasoning_content، لكن داخليًا، تستمر نماذج اللغة الكبيرة فعليًا في توليد تسلسل واحد من الرموز. نستخدم طبقة وسيطة لربط واجهة برمجة التطبيقات الموجهة للمستخدم بتدفق الرموز الفعلي للنموذج. لتحويل المدخلات إلى تدفق رموز، نستخدم chat_templates، وهي موضحة في مستندات أخرى. يتناول هذا المستند النصف الآخر من تلك الطبقة الوسيطة: Response templates، وهو النظام الذي يعيد تحويل الرموز المولدة من النموذج إلى قاموس استجابة مهيكلة.

بعدة طرق، تؤدي قوالب الاستجابة العملية العكسية لقوالب المحادثة. ففي قوالب المحادثة، تُدخل قائمة من الرسائل، فتحصل على رموز جاهزة للإدخال إلى النموذج. أما في قوالب الاستجابة، فتُدخل رموز المخرجات الخام للنموذج، فتحصل على رسالة منظمة. وكما هو الحال مع قوالب المحادثة، تتيح قوالب الاستجابة للمستخدمين تجاهل التفاصيل المعقدة المتعلقة بالصيغ المحددة ورموز التحكم التي يتوقعها النموذج، والاعتماد على واجهة برمجية موحدة تعتمد على قواميس الرسائل التي تعمل مع أي نموذج.

أفضل طريقة لفهم قوالب الاستجابة هي رؤيتها قيد العمل. نقطة الدخول الرئيسية هي طريقة parse_response()، والتي تقبل تسلسلاً واحداً أو دفعة:

from transformers import AutoModelForCausalLM, AutoTokenizer

checkpoint = "HuggingFaceTB/SmolLM3-3B"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForCausalLM.from_pretrained(checkpoint, dtype="auto", device_map="auto")

messages = [{"role": "user", "content": "Summarize the end of the Cold War, very briefly."}]
input_ids = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt")["input_ids"].to(model.device)
outputs = model.generate(input_ids, max_new_tokens=1024)[0, input_ids.shape[1]:]
out_text = tokenizer.decode(outputs)
print(tokenizer.parse_response(out_text, prefix=input_ids[0]))
# Outputs a structured dict: {"role": "assistant", "reasoning_content": "...", "content": "..."}

عندما يحتوي مُجزِّئ الرموز على response_template، فإن الطريقة parse_response ستحوّل رسالة الإخراج بشكل نظيف إلى قاموس مُنظَّم، جاهز للإضافة إلى المحادثة. لاحظ أنه يجب علينا تمرير prefix (رموز الاستدعاء) إلى هذه الطريقة أيضًا. والسبب في ذلك أن العديد من قوالب المحادثة تبدأ الرسائل أو تفتح كتل التفكير قبل السماح للنموذج ببدء استجابته، وبالتالي يحتاج مُحلِّلنا إلى رؤية الاستدعاء لفهم الرسالة. يتم تجاهل كل ما يسبق الجولة الأخيرة من البادئة؛ فنحن نحلل رسالة واحدة في كل مرة. نحتاج فقط إلى البادئة للتأكد من أننا نرى الرسالة الأخيرة بأكملها، ولا نفوت أي حقول مُعبَّأة مسبقًا!

بما أن غياب البادئة قد يؤدي إلى تحليل خاطئ بصمت لرسالة مُعبّأة مسبقًا، فإن prefix إلزامي. يؤدي حذفه إلى رفع خطأ. في الحالة النادرة التي تكون فيها متأكدًا من أن التوليد يحتوي بالفعل على الرسالة الكاملة ولا يُحتاج إلى سياق بادئة، مرّر prefix="" (أو قائمة فارغة من معرّفات الرموز) للتخلّي عن ذلك صراحةً.

إذا لم يكن مُجزّئ الرموز (tokenizer) مُهيّأً بقالب استجابة، فسيُطلق parse_response خطأً. نحن نعمل على إضافة قوالب إلى المزيد من النماذج بأسرع ما يمكن!

تحليل الاستجابة البث

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

عندما ترغب في التحليل التدفقي، استدعِ tokenizer.get_response_parser()، الذي يعيد ResponseParser. كما هو الحال مع parse_response، مرّر موجه الدردشة كـ prefix= حتى يعرف المحلل أي أجزاء من الرسالة تم ملؤها مسبقًا بواسطة قالب الدردشة. الكائن المُعاد هو محلل ذو حالة يمكنك إمداده بالنص أثناء توليده من قبل النموذج:

parser = tokenizer.get_response_parser(prefix=input_ids[0])
for event in parser.initial_events:
    render(event)  # Display the partial message to the user however you want to
for chunk in model_output:
    for event in parser.feed(chunk):
        render(event)
message, final_events = parser.finalize()
for event in final_events:
    render(event)

إذا تضمن الطلب أدوات، فمررها أيضًا (get_response_parser(..., tools=tools)). تُحدد أنواع حُجج استدعاء الأدوات من مخطط JSON Schema للأداة المستدعية عند إغلاق كل منطقة، بحيث ترى مستهلكات البث حُججًا محددة الأنواع حسب المخطط عند region_close وليس فقط بعد finalize(). راجع تحديد أنواع حُجج استدعاء الأدوات للتفاصيل.

سيصدر المُحلّل أحداثًا كنص أثناء تغذية النص الناتج عن عملية التوليد. وهذا يُظهر المنطقة التي يجري توليدها حاليًا. عند اكتمال المنطقة، ستُصدر في حدث منفصل مع المحتوى المُحلَّل بالكامل. وفي نهاية التوليد، تُفرغ طريقة finalize() أي نص متبقٍ وتصدر أي أحداث نهائية، بالإضافة إلى قاموس الرسالة الكامل.

لاحظ أنه على الرغم من أن parse_response يمكنه قبول دفعات (batches)، فإن التحليل المتدفق يكون دائمًا لسلسلة واحدة: يتتبع كل ResponseParser حالة توليد واحد. إذا كنت ترغب في بث عدة عمليات توليد في وقت واحد، أنشئ ResponseParser واحدًا لكل سلسلة.

بث الأحداث

كل حدث تحليل مُبَثّ هو قاموس (dict) بمفتاح type. هناك ثلاثة أنواع:

النوعالوصفالمحتويات
region_openيشير إلى أن النموذج بدأ منطقة جديدة، مثل content أو reasoning_content.field (str): اسم الحقل.
region_chunkمقطع نصي للمنطقة الحالية.field (str): اسم الحقل. text (str): المقطع الجديد. dirty (bool): True إذا كان المقطع نصًا خامًا يحتاج إلى تحليل.
region_closeيشير إلى أن منطقة ما قد انتهت، وأن المفتاح أصبح الآن نهائيًا.field (str): اسم الحقل. value (any): القيمة المُحلَّلة بالكامل للمنطقة

تُصدر أحداث region_chunk لكل منطقة مع وصول البايتات، مما يتيح لواجهة المستخدم البثية عرض التقدم حتى للمناطق المهيكلة. بالنسبة للمناطق المشابهة للنص (text، int، float، bool)، تُوسم المقاطع بـ dirty=False: فكل مقطع هو بالفعل جزء من القيمة النهائية (باستثناء المسافات البادئة التي تُزال عند الإغلاق). أما بالنسبة للمناطق المهيكلة، مثل استدعاءات الأدوات بتنسيق JSON، فتُوسم المقاطع بـ dirty=True. وهذا يعني أن النص هو الجسم الخام غير المُحلَّل؛ فهو آمن للعرض التدريجي، لكن القيمة المُحلَّلة (قاموس، قائمة، إلخ) لا تصل إلا في حدث region_close المقابل. في كلتا الحالتين، تحمل القيمة النهائية للم منطقة دائمًا حدث region_close، لذا يمكن للمستهلكين الذين لا يهتمون بالعرض الوسيط تجاهل أحداث region_chunk ببساطة.

إذا كتب بادئة المحادثة prefix أي شيء في الرسالة (مثل فتح قالبٍ لكتلة تفكير، أو بدء استجابة مسبقة من المساعد قبل التسليم إلى النموذج)، فإن المُحلِّل يعرض تلك الأحداث على شكل parser.initial_events، وهي قائمة يمكنك إعادة تشغيلها في عارضك قبل إمداد أي مخرجات من النموذج. المناطق التي فُتحت و أُغلقت داخل البادئة تنتج تسلسلاً كاملاً من region_open / region_chunk / region_close، وتقع قيمتها المُحلَّلة في قاموس المخرجات، تماماً كما لو كان النموذج نفسه قد كتبها.

قد يبدو تدفق الأحداث النموذجي على النحو التالي:

{"type": "region_open",  "field": "reasoning_content"}
{"type": "region_chunk", "field": "reasoning_content", "text": "I should ", "dirty": False}
{"type": "region_chunk", "field": "reasoning_content", "text": "greet the user", "dirty": False}
{"type": "region_close", "field": "reasoning_content", "value": "I should greet the user"}
{"type": "region_open",  "field": "tool_calls"}
{"type": "region_chunk", "field": "tool_calls", "text": '{"name": "greet_user", ', "dirty": True}
{"type": "region_chunk", "field": "tool_calls", "text": '"arguments": {"greeting": "Hi!"}}', "dirty": True}
{"type": "region_close", "field": "tool_calls", "value": {"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}}}

لاحظ كيف يُصدر reasoning_content مع dirty=False، لأن حقولاً مثل reasoning_content وcontent تكون عادةً مجرد نص خام. وهذا يعني أنه يمكنك التعامل مع المقاطع كـ "إخراج جزئي" صالح. ومع ذلك، يُعلَّم tool_calls بأنه dirty لأن النص الخام يحتاج إلى تنظيف كبير - فغالباً ما تحتاج استدعاءات الأدوات إلى التحليل كـ JSON أو تنسيق آخر ثم إعادة هيكلتها لتوليد قاموس استدعاء الأداة النهائي. ونتيجةً لذلك، يبدو الإخراج النهائي لهذه المناطق مختلفاً جداً، جداً عن النص الخام. لن يحدث هذا التحليل النهائي إلا عند الوصول إلى region_close. الأمر متروك لك فيما تريد فعله بمقاطع dirty حتى ذلك الحين - يمكنك عرضها كما هي لإظهار الإخراج "الخام" للمستخدم، أو يمكنك ببساطة الانتظار حتى تحصل على شيء نظيف لعرضه.

هذا يُنهي معظم ما تحتاج إلى معرفته لاستخدام قوالب الاستجابة. يركّز بقية هذا المستند على التفاصيل الداخلية لنظام التحليل وكيفية كتابة قوالب الاستجابة. وهذا ذو صلة بالدرجة الأولى للمطوّرين ومطوّري النماذج. يمكن لمعظم الأشخاص التوقف هنا بأمان!

متقدم: كتابة قالب استجابة

أفضل طريقة لفهم كيفية كتابة قالب استجابة هي اختيار مثال محدد. إليك ما قد يبدو عليه رد خام من SmolLM:

<think>
I should greet the user
</think>

<tool_call>{"name": "greet_user", "arguments": {"greeting": "Hi!"}}</tool_call>

عند تحليل هذا المخرج في صيغة قاموس الرسائل القياسية، يجب أن يبدو على النحو التالي:

{
    "role": "assistant",
    "reasoning_content": "I should greet the user",
    "tool_calls": [
        {"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}}
    ]
}

وهذه هي القالب الذي يحلله. لا تدع الأمر يُخيفك - فمعظمه واضح إلى حد كبير!

{
    "defaults": {"role": "assistant"},
    "start_anchor": "<|im_start|>assistant\n",
    "fields": {
        "reasoning_content": {"open": "<think>", "close": "</think>", "content": "text"},
        "tool_calls": {
            "open": "<tool_call>",
            "close": "</tool_call>",
            "repeats": True,
            "content": "json",
            "transform": {"type": "function", "function": "{content}"},
        },
        "content": {
            "close": "<|im_end|>",
            "content": "text",
        },
    },
}

في جوهره، يحدد القالب الحقول والفواصل. يتوافق كل حقل مع مفتاح في قاموس المخرجات. كما تتضمن الحقول معلومات حول تحليل النص الموجود داخل فواصلها. هناك تفصيل دقيق: لا يحتوي حقل content على open، لأنه في نموذج SmolLM (وبعض النماذج الأخرى) لا يُشار إليه برمز خاص. بدلاً من ذلك، يُخزَّن content في المساحة بعد المناطق الأخرى، ولكن قبل نهاية التسلسل. في قالبنا، نمثل هذا كحقل ضمني / متبقٍ يلتقط أي نص لم يتم ادعاؤه بواسطة منطقة أخرى.

يجب أن تتطابق أسماء الحقول مع المفاتيح التي يقرأها قالب المحادثة الخاص بالنموذج، بحيث يمكن إضافة رسالة محللة إلى المحادثة وعرضها مرة أخرى. على وجه الخصوص، سمِّ حقل الاستدلال reasoning_content. هذا هو المفتاح القياسي للاستدلال في قواميس الرسائل، وهو المفتاح الذي تقرأ منه معظم قوالب المحادثة الاستدلال.

بالإضافة إلى fields، يدعم القالب مفتاحين على المستوى الأعلى:

  • defaults (اختياري) — قاموس بقيم مُعبّأ بها مسبقًا في المخرجات (مثل {"role": "assistant"}). المفاتيح هنا تُحتفظ دائمًا في المخرجات المُحلَّلة، حتى لو لم تكتب أي حقل إليها؛ أما المفاتيح الأخرى فتُستبعد عندما لا يلتقط حقلها أي شيء.
  • start_anchor (str) / start_anchor_pattern (str regex) — يحدد مكان بداية رسالة المساعد الحالية داخل محادثة. عند تمرير prefix= إلى parse_response أو get_response_parser، يقوم المُحلِّل بقصّ البادئة من اليمين بعد آخر ظهور لهذا المرساة قبل معالجتها، حتى لا تُلوِّث الأدوار السابقة في محادثة متعددة الأدوار حالة الرسالة الحالية. تُطبَّق المرساة على prefix فقط، ولا تُطبَّق أبدًا على الاستجابة/التوليد الذي تُحلِّله — فبعض الصيغ تعيد إصدارها بشكل مشروع في منتصف الرسالة (تبدأ مخرجات gpt-oss harmony كل قناة بـ <|start|>assistant)، لذا فإن قصّ الاستجابة بعد المرساة سيؤدي إلى إسقاط استدعاءات النموذج الخاصة بالاستدلال والأدوات. ولهذا السبب لا يكفي التوليد وحده للحماية من تسرّب التاريخ: مرِّر المحادثة كـ prefix=. بالنسبة للنماذج بنمط ChatML، تكون المرساة عادةً "<|im_start|>assistant\n". يجب تعيين واحد فقط من start_anchor أو start_anchor_pattern.

على سبيل المثال، بالنظر إلى هذا البادئ متعدد الأدوار (لاحظ الدورين للوكيل):

<|im_start|>user
Hi<|im_end|>
<|im_start|>assistant
Hello!<|im_end|>
<|im_start|>user
Again?<|im_end|>
<|im_start|>assistant

يقوم المُحلِّل بقصّ كل ما يسبق آخر <|im_start|>assistant\n، متجاهلاً الدور السابق "Hello!". القاعدة هي أن كل شيء عدا الدور الأخير للمساعد يُستبعد دائماً.

كما هو الحال مع قوالب المحادثة، تُخزَّن قوالب الاستجابة كخصائص لمُجزِّئ النص (tokenizer) وتُحفظ مع المُجزِّئ. وعلى عكس قوالب المحادثة، نحفظها داخل tokenizer_config.json وليس كملف منفصل، لأن صيغتها تتناسب بشكل طبيعي مع JSON، على عكس سكربت Jinja الخاص بقالب المحادثة.

tokenizer.response_template = template
tokenizer.save_pretrained(...)  # Written as a key in tokenizer_config.json

متقدم: مرجع واجهة برمجة التطبيقات للحقول

يدعم كل حقل عدة مفاتيح. يمكننا تقسيمها إلى نوعين. أولاً، هناك المفاتيح التي تحدد كيفية التقاط الحقل:

المفتاحالنوعالغرض
openstr or list[str]سلسلة نصية حرفية تفتح هذه المنطقة. قائمة من السلاسل النصية تعني «المطابقة مع أيٍّ من هذه».
open_patternstr (regex)بديل open باستخدام التعبيرات النمطية. تصبح المجموعات المسماة متغيرات التقاط متاحة لـ transform.
closestr or list[str]سلسلة نصية حرفية (أو قائمة من السلاسل النصية) تُغلق هذه المنطقة. اتركها فارغة للتشغيل حتى نهاية التدفق.
close_patternstr (regex)بديل close باستخدام تعبيرات نمطية. تصبح المجموعات المسماة متغيرات التقاط متاحة لـtransform.
repeatsboolإذا كانت القيمة صحيحة، يكون الحقل قائمة ويُلحق كل تطابق. الافتراضي false.
joinstrمع repeats، يتم دمج التطابقات في سلسلة واحدة باستخدام هذا الفاصل بدلاً من قائمة.
optionalboolIf false and the region never matches, we raise an error. Default true.

يجب أن يحتوي الحقل على إما open أو open_pattern، وليس كلاهما معاً، وينطبق الأمر نفسه على close وclose_pattern.

قد يُستبعد close/close_pattern بالكامل من حقل ما، وفي هذه الحالة يبقى المجال مفتوحًا حتى نهاية النص المولّد. وهذا مفيد لحقل نهائي يمتد حتى نهاية الرسالة.

الحقل الذي لا يحتوي على أيٍّ من open أو open_pattern هو الحقل ضمني: فهو نشط في أي وقت لا يكون فيه أي نطاق صريح مفتوحًا، لذا يلتقط النص المتبقي. لا يمكن أن يكون هناك أكثر من حقل ضمنٍ واحد. يُستخدم هذا غالبًا عندما لا يحتوي content على وسوم رموز خاصة، بل يُكتب كنص عادي بعد الحقول الأخرى.

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

'<tool_call>{"name": "a", ...}</tool_call><tool_call>{"name": "b", ...}</tool_call>'
# Returns `"tool_calls": [{... "a" ...}, {... "b" ...}]` in a template with repeats: true

لكن ليس كل حقل متكرر ينبغي أن يتحول إلى قائمة. فبعض النماذج تُخرج عدة كتل reasoning_content أو content في رسالة واحدة، والمخرج الطبيعي لذلك هو نص واحد مدمج. ضبط join (نص فاصل، وغالبًا ما يكون "") يُبدّل حقل repeats من جمع قائمة إلى دمج تطابقاته:

field = {"reasoning_content": {"open": "<think>", "close": "</think>", "repeats": True, "join": "\n"}}
input = "<think>first</think>...<think>second</think>"
# Returns: {"reasoning_content": "first\nsecond"}

يجب أن يُحلَّل كل تطابق لحقل join إلى نص. عند البث، لا يزال region_close يحمل قيمة كل كتلة على حدة؛ بينما يظهر الفاصل فقط في قاموس الرسالة النهائي.

أخيرًا، يمكنك تحديد optional: false للحقول التي يجب أن تكون موجودة. إذا كان مثل هذا الحقل مفقودًا، فسنُطلق خطأً بدلاً من مجرد إرجاع قاموس رسالة دون تضمينه.

عند انتهاء التوليد، سيتم إغلاق أي مناطق مفتوحة وتثبيتها، حتى لو لم يُرَ فاصل الإغلاق الخاص بها.

تحليل محتوى حقل

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

المفتاحالنوعالغرض
contentstrنوع المحتوى داخل هذه المنطقة. القيمة الافتراضية هي "text". لكل نوع محلّل خاص به.
content_argsdictالحُجج المراد تمريرها إلى محلّل المحتوى لهذه المنطقة.
transformdict/listقالب اختياري بعد التحليل يعيد تشكيل الجسم المُحلَّل (انظر Transform).
transform_eachboolإذا كانت القيمة صحيحة، يجب أن يكون المحتوى المُحلَّل قائمة، ويُطبَّق transform على كل عنصر على حدة.

المفتاح الأول (والأهم) هو content. يشير هذا إلى نوع محتوى الحقل، وهو ما يحدد المُحلِّل الذي سيُستخدم لتحويل النص الخام المُلتقط في الحقل إلى المخرجات النهائية. تُستخدم content_args لإعداد المُحلِّل، وتتيح لنا دعم اختلافات التنسيق المختلفة دون الحاجة إلى كتابة أكواد مخصصة. سنلقي نظرة على كل نوع من أنواع المُحلِّلات وحواملها بالتتابع.

الأنواع الأساسية

text وint وfloat وbool هي الأنواع الأساسية. جميع أنواع المحتوى هذه تزيل المسافات البيضاء فقط ثم تقوم بتحويل بسيط للنوع عند الحاجة. لا تحتوي على أي content_args، باستثناء text الذي يدعم الحجة strip، والتي تزيل المسافات البيضاء من بداية النص الملتقط ونهايته، وتكون القيمة الافتراضية لها true.

field = {"count": {"open": "<n>", "close": "</n>", "content": "int"}}
input = "<n> 42 </n>"
# Returns: {"count": 42}

json

يقوم محلل json بتحليل النص المُلتقط بصيغة JSON. وهو الأداة الأساسية لمعالجة حُجج استدعاء الأدوات وأي شيء آخر ذي بنية متداخلة. كما يقبل مجموعة صغيرة من content_args الاختيارية للتعامل مع الطرق المختلفة التي تشوّه بها النماذج صيغة JSON في الاستخدام الفعلي:

  • unquoted_keys (bool، الافتراضي false): فعّل هذا الخيار عندما تكون أسماء المفاتيح خامًا بدلاً من أن تكون محاطة بعلامات اقتباس (مثل {name: "foo"}). مفيد للنماذج التي تُخرج كائنات بأسلوب جافاسكربت بدلاً من JSON الصارم.
  • string_delims (قائمة من أزواج [open, close]، اختيارية): للنماذج التي تغلف القيم النصية بأحرف فاصلة مخصصة بدلاً من "...". يمنح كل زوج علامة افتتاحية وعلامة ختامية.
  • allow_non_json (bool، الافتراضي false): إذا فشل التحليل، يُعاد النص الخام بعد تنظيفه بدلاً من رفع استثناء. مفيد كحل بديل للحقول التي يُصدر النموذج منها JSON عادةً لكنه قد ينتقل أحياناً إلى نص عادي.

يوجد unquoted_keys وstring_delims معًا للتعامل مع النماذج التي تُخرج مخرجات غير قياسية، وهي شبه JSON لكنها ليست كذلك تمامًا، لذا لن تحتاج إليها إلا لعدد قليل من النماذج.

field = {"args": {"open": "<args>", "close": "</args>", "content": "json", "content_args": {"unquoted_keys": True}}}
input = '<args>{city: "London"}</args>'
# Returns: {"args": {"city": "London"}}

xml-inline

يُستخدم مُحلّل xml-inline للمناطق المكوّنة من تسلسل مسطّح من وسوم تشبه وسوم XML، حيث يصبح كل وسم عنصراً واحداً في قاموس. يُستخدم هذا المُحلّل في الغالب داخل حقل tool_calls للنماذج التي تُصدر كل حجة كوسم مستقل بدلاً من كتلة JSON:

  • tag_pattern (str، مطلوب): تعبير نمطي (regex) يطابق وسمًا واحدًا. يجب أن يحتوي على مجموعات مسماة key (مفتاح القاموس الناتج) وvalue (النص الخام الذي يصبح قيمة القاموس).
  • value_parser (dict, اختياري): محلل محتوى متداخل يُطبَّق على كل value تم التقاطه. قاموس يحتوي على name (المحلل، مثل "json"، "int") وargs اختياري (أي content_args الخاصة به). إذا تم تجاهله، تبقى القيم كنصوص خام.
  • merge_duplicates (bool، الافتراضي false): عندما يظهر المفتاح نفسه عدة مرات، يتم جمع القيم في قائمة بدلاً من السماح للمطابقات اللاحقة بتجاوز السابقة.

على سبيل المثال، تُصدر Qwen3 كل حجة لاستدعاء الأداة كوسم <parameter> مستقل، ونحلله على النحو التالي:

"tool_calls": {
    "open_pattern": r"<tool_call>\s*<function=(?P<name>\w+)>",
    "close": "</tool_call>",
    "repeats": True,
    "content": "xml-inline",
    "content_args": {
        "tag_pattern": r"<parameter=(?P<key>\w+)>\s*(?P<value>.*?)\s*</parameter>",
        "value_parser": {"name": "json", "args": {"allow_non_json": True}},
    },
    "transform": {"type": "function", "function": {"name": "{name}", "arguments": "{content}"}},
}

لاحظ value_parser المتداخل: يُمرَّر كل قيمة معيار عبر مُحلِّل json (مع allow_non_json حتى تمر النصوص العادية كما هي). عند تغذية حقل tool_calls أعلاه بهذا المدخل:

input = "<tool_call><function=get_weather><parameter=city>London</parameter><parameter=units>celsius</parameter></function></tool_call>"
# Returns: {"tool_calls": [{"type": "function", "function": {"name": "get_weather", "arguments": {"city": "London", "units": "celsius"}}}]}

kv-lines

يتعامل محلل kv-lines مع أزواج key: value مفصولة بالسطر (تخيل بيانات وصفية تشبه YAML أو ملفات .env). يصبح كل سطر عنصراً واحداً في القامس الناتج. جميع الحجج اختيارية:

  • line_sep (str، الافتراضي "\n"): الفاصل بين الأزواج.
  • kv_sep (str، الافتراضي ":"): الفاصل بين المفتاح وقيمته داخل سطر واحد. يُستخدم أول ظهور فقط كنقطة تقسيم، لذا قد تحتوي القيم نفسها على الفاصل.
  • strip (bool، الافتراضي true): إزالة المسافات البيضاء المحيطة من كل مفتاح وقيمة.
  • value_parser (dict, اختياري): محلل محتوى متداخل يُطبَّق على كل قيمة، بنفس تنسيق {"name": ..., "args": ...} المستخدم في xml-inline. إذا تم تجاهله، تبقى القيم كنصوص خام.

تُتجاهل الأسطر الفارغة أو التي لا تحتوي على kv_sep بصمت، لذا يُسمح بحدوث أسطر فارغة زائدة في المنطقة الملتقطة.

field = {"metadata": {"open": "<meta>", "close": "</meta>", "content": "kv-lines"}}
input = "<meta>name: alice\nage: 30</meta>"
# Returns: {"metadata": {"name": "alice", "age": "30"}}

ملاحظة age يحافظ على "30" كنص؛ أضف value_parser بقيمة {"name": "int"} لتحويله إلى 30.

نمذجة حُجج استدعاء الأدوات

في بعض الأحيان، قد يقوم محلل الاستجابة بتحليل مخرجات النموذج بالنوع الخاطئ. على سبيل المثال، قد يحلل الرقم العشري 1.5 على أنه “1.50”. قد يتسبب ذلك في مشاكل مع استدعاء الأدوات، إذا كانت الأدوات تتوقع حجة بنوع معين لكنها تستلمها بنوع آخر. لتجنب ذلك، يمكنك تمرير tools الخاصة بالطلب إلى parse_response() أو ResponseParser لتحويلها باستخدام parameters في مخطط JSON الخاص بكل أداة. تُقبل الأدوات بنفس التنسيق المستخدم في apply_chat_template(): مخططات JSON، أو دوال بايثون مع تلميحات أنواع ووثائق تُحوَّل تلقائيًا إلى مخططات.

tools = [{
    "type": "function",
    "function": {
        "name": "set_alarm",
        "parameters": {
            "type": "object",
            "properties": {
                "hour": {"type": "integer"},
                "enabled": {"type": "boolean"},
                "label": {"type": "string"},
            },
        },
    },
}]
message = parse_response(model_out, template, prefix="", tools=tools)
# message["tool_calls"][0]["function"]["arguments"] ==
# {"hour": 7, "enabled": True, "label": "wake up"}

تحويل

في معظم الحقول، يكون المفتاح transform غير ضروري. يُستخدم عندما يحتاج الجسم المُحلَّل إلى إعادة تشكيله ليصبح المخرجات النهائية، أو عندما يجب دمج المعلومات من الفواصل في النتيجة. ويظهر بشكل شائع في حقول tool_calls، نظراً لأن هذه الحقول غالباً ما تكون ذات بنية معقدة.

transform هو قالب: وهو قاموس (أو قائمة) يصف شكل المخرجات، حيث يتم استبدال أي نص على شكل "{name}" بالقيمة المقابلة. يمكن الوصول إلى القيم من content (جسم هذا الجزء بعد تحليله) ومن أي مجموعات مسماة تم التقاطها بواسطة open_pattern / close_pattern. من الاستخدامات الشائعة جداً تغليف قاموس استدعاء الأداة داخل قاموس خارجي يحتوي على مفتاح function، حيث أن هذه العناصر جزء من صيغة استدعاء الأدوات القياسية لدينا:

"tool_calls": {
    "open": "<tool_call>",
    "close": "</tool_call>",
    "repeats": True,
    "content": "json",
    "transform": {"type": "function", "function": "{content}"},
},

لذا فإن هذا المخرج الخام:

<tool_call>{"name": "greet_user", "arguments": {"greeting": "Hi!"}}</tool_call>

يصبح (لاحظ أن repeats: True يجعل tool_calls قائمة):

[{"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}}]

يعود مُعرّف كامل السلسلة مثل "{content}" بالقيمة المُسترجَعة مع الحفاظ على نوعها — لذا في المثال أعلاه، تُدرَج قاموس JSON المُحلَّل مباشرةً كقيمة لـ function. يجب أن يكون المُعرّف هو السلسلة بأكملها: لا يُسمح بخلط النص مع المُعرِّفات ("abc {name} def"). إنها ليست سلاسل f!

يمكن أن تتخذ العلامات الموضعية أيضًا مسارًا منقّطًا مثل "{content.args}"، حيث يبحث عن content ثم ينزل إلى مفتاح args في القاموس المُحلَّل. هذه هي الطريقة التي تعيد بها تشكيل أجسام استدعاءات الأدوات التي لا تتطابق أسماء مفاتيحها مع صيغتنا القياسية. على سبيل المثال، يحتاج النموذج الذي يُصدر {"name": ..., "args": ...} إلى إعادة تسمية args إلى arguments:

"tool_calls": {
    "open": "<tool>",
    "close": "</tool>",
    "repeats": True,
    "content": "json",
    "transform": {"type": "function", "function": {"name": "{content.name}", "arguments": "{content.args}"}},
},

تُثير المسار الذي لا يُحل (هنا، استدعاء أداة يفتقر جسمه إلى args) خطأً بدلاً من إصدار استدعاء أداة غير صالح.

يتميز transform بمرونة كبيرة، وهو أمر ضروري عندما يكون تنسيق مخرجات النموذج مختلفًا بشكل كبير عن واجهة برمجة التطبيقات القياسية لدينا. يُعد GPT-OSS مثالًا جيدًا على ذلك - فهو يدمج اسم الدالة في ترويسة القناة بدلاً من جسم JSON، لذا يجب التقاطه بمجموعة مسمّى داخل open_pattern ودمجه مع content داخل التحويل. تصبح جميع المجموعات المسمّى في open_pattern وclose_pattern متاحة كمتغيرات إلى جانب content:

"tool_calls": {
    "open_pattern": r"<\|channel\|>commentary to=functions\.(?P<name>\w+).*?<\|message\|>",
    "close": "<|call|>",
    "repeats": True,
    "content": "json",
    "transform": {"type": "function", "function": {"name": "{name}", "arguments": "{content}"}},
},

يوجد اسم الدالة في ترويسة القناة وليس في جسم JSON، لذا فإن هذا:

<|channel|>commentary to=functions.get_current_weather <|constrain|>json<|message|>{"location": "San Francisco, CA"}<|call|>

يصبح:

[{"type": "function", "function": {"name": "get_current_weather", "arguments": {"location": "San Francisco, CA"}}}]

في بعض الأحيان يكون المحتوى المُحلَّل لحقلٍ ما هو نفسه قائمة من السجلات وتريد إعادة تشكيل كل عنصر منها. قالب Cohere مثال جيد على ذلك: فهو يُخرج جميع استدعاءات الأدوات داخل مصفوفة JSON واحدة، لذا نضبط transform_each: True لتطبيق التحويل لكل عنصر على حدة. تُفكَّ مفاتيح كل عنصر في المصفوفة إلى نطاق القالب، لذا يبحث "{tool_name}" عن tool_name في العنصر الحالي:

"tool_calls": {
    "open": "<|START_ACTION|>",
    "close": "<|END_ACTION|>",
    "content": "json",
    "transform_each": True,
    "transform": {"type": "function", "function": {"name": "{tool_name}", "arguments": "{parameters}"}},
},

سيؤدي هذا إلى تحويل مخرجات على النحو التالي:

[
    {"tool_name": "greet_user", "parameters": {"greeting": "Hi!"}},
    {"tool_name": "search", "parameters": {"query": "weather tomorrow"}}
]

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

[
    {"type": "function", "function": {"name": "greet_user", "arguments": {"greeting": "Hi!"}}},
    {"type": "function", "function": {"name": "search", "arguments": {"query": "weather tomorrow"}}}
]

لا يُعدّ علم transform_each ضرورياً إلا عندما يكون content قائمة بالفعل؛ أما في الحالة الأكثر شيوعاً حيث يساهم كل تطابق بعنصر واحد (وتتراكم العناصر بواسطة repeats: True)، فإن التحويل يُطبَّق على كل عنصر افتراضياً.

مطوّرو الإطارات: قابلية نقل تعبيرات النمط

open_pattern وclose_pattern وstart_anchor_pattern هي نصوص تعبيرات منتظمة. لمعظم المستخدمين، وحتى لمعظم مؤلفي النماذج، لا ينبغي أن يكون هذا مشكلة، ولكن إذا كنت مطوّرًا تكتب تنفيذًا لتحليل الاستجابات بلغة أخرى، فيجب أن تكون على دراية بتفاصيل التنفيذ لدينا. هذا القسم مخصص لكل من اضطر إلى تنفيذ محلّل Jinja كامل لجعل قوالب الدردشة غير المكتوبة بلغة بايثون تعمل - نأمل أنه إذا اتبعت الإرشادات البسيطة أدناه، فستكون قوالب الاستجابة أقل إزعاجًا بكثير:

  • نستخدم وحدة regex في بايثون لجميع تعبيرات النمط المستخدمة في تحليل المحادثات. بما أن جميع نصوص Python3 هي نصوص يونيكود، فإن جميع تطابقات تعبيرات النمط لدينا تدعم يونيكود. يؤثر هذا بشكل خاص على الأحرف الشائعة مثل \w. تأكد من ضبط أعلام يونيكود ذات الصلة في محركك.
  • نجمع جميع تعبيرات النمط المنتظم مع تفعيل re.DOTALL وتعطيل re.MULTILINE، لذا يطابق . الرمز \n بينما يطابق ^ و$ بداية/نهاية المدخلات بأكملها فقط، وليس نهايات الأسطر.
  • نستخدم صيغة (?P<name>...) للمجموعات المسماة. تختلف صيغة مجموعات الالتقاط المسماة بشكل كبير في تطبيقات أخرى من تطبيقات تعبيرات النمط، لذا قد تحتاج إلى البحث عن هذا النمط في تعبيرات النمط وإعادة كتابته ليتوافق مع تطبيقك المحلي.
  • نستخدم المطابقة الجزئية للتعبيرات النمطية (regex) لتحديد متى يمكننا إصدار البيانات. وهذا ضروري لأن نهاية المنطقة قد تتكون من عدة رموز (tokens)، ولا نريد إصدار فاصل نهاية المنطقة، لذا نحتفظ بالرموز حتى نتأكد من أنها داخل المنطقة وليست جزءًا من الحد الفاصل. وهذا يعني أنه كلما كانت هناك مطابقة جزئية لتعبير نمطي لنهاية منطقة، نحتفظ بالبيانات حتى يتطابق التعبير النمطي أو يفشل. إذا لم يدعم محرك التعبيرات النمطية الخاص بك المطابقة الجزئية، يمكنك ما زال تنفيذ قوالب الاستجابة، لكن قد تحتاج إلى إيجاد حل آخر لهذه المشكلة. إحدى البسيطة هي الاحتفاظ بهذه المناطق / إصدارها كـdirty حتى ترى فاصل النهاية بشكل قاطع.
  • قد لا يدعم محرك التعبيرات النمطية الخاص بك (في حالات نادرة) أنماط الفحص المحيطي مثل (?!...). وعلى الرغم من أن هذه الأنماط لا تُستخدم بشكل شائع في قوالب الاستجابة، إلا أنها قد تظهر، ونحن ندعمها! قد تحتاج في تلك الحالات إما إلى إلقاء خطأ، أو إلى استخراج أنماط الفحص المحيطي يدويًا وفرضها في كودك عندما يجد محرك التعبيرات النمطية تطابقًا محتملًا.
  • الميزات المتقدمة الأخرى مثل المراجع الخلفية (backreferences) والمجموعات الذرية (atomic groups) والمعدِّلات الحصرية (possessive quantifiers) والتكرار الذاتي (recursion) وما إلى ذلك، لا تُستخدم عادةً في قوالب الاستجابة. سنحاول إقناع مصنّعي النماذج بعدم استخدامها، لذا يمكنك على الأرجح تجاهلها بأمان.
Update on GitHub