%pip install -q openaiلتشغيل النسخة العربية، نزّل الملف وافتحه في Jupyter أو ارفعه إلى Google Colab. ويمكنك أيضًا فتح الدفتر الإنجليزي الأصلي في Colab.
التعليمات هنا بالعربية. أبقينا البرامج وتعليقاتها والطلبات التجريبية كما هي لتظل التجارب مطابقة للنسخة الأصلية.
الفصل 1 — الدفتر البرمجي
دفتر للتطبيق أثناء الدرس. أكمل المواضع المعلّمة بـTODO خلال التجارب.
الإعداد
- أنشئ حسابًا في openrouter.ai.
- أنشئ مفتاح API.
- استبدل
YOUR_OPENROUTER_API_KEY_HEREفي خلية الإعداد بمفتاحك محليًا. لا تشارك دفترًا يحتوي على مفتاحك.
قد تتغيّر إتاحة النماذج وأسماؤها؛ إذا تعذّر استدعاء أحدها، اختر نموذجًا متاحًا من الخدمة.
import os
from openai import OpenAI
api_key = "YOUR_OPENROUTER_API_KEY_HERE" # Replace with your actual key
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=api_key,
)
def ask(model: str, prompt: str, temperature: float = 0.7, max_tokens: int = 300) -> None:
"""Stream a single-turn prompt. Prints tokens as they arrive"""
stream = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=temperature,
max_tokens=max_tokens,
extra_body={
"reasoning": {
"effort": "low",
"exclude": False
}
},
stream=True,
)
thinking_started = False
for event in stream:
choice = event.choices[0]
reasoning = getattr(choice.delta, "reasoning_content", None) or getattr(choice.delta, "reasoning", None) or ""
if reasoning:
if not thinking_started:
print("💭 Thinking: ", end="", flush=True)
thinking_started = True
print(reasoning, end="", flush=True)
delta = choice.delta.content or ""
if delta:
if thinking_started:
print("\n\n📝 Answer: ", end="", flush=True)
thinking_started = False
print(delta, end="", flush=True)
print()
# Some free models on OpenRouter (check the site for the current list):
FREE_MODELS = [
"nvidia/nemotron-3-super-120b-a12b:free",
"minimax/minimax-m2.5:free",
"stepfun/step-3.5-flash:free"
]
PAID_MODELS = [
"openai/gpt-oss-20b",
"openai/gpt-4o-mini", # supports temperature, will be sunset by openai soon
"openai/gpt-5.4", # doesn't support temperature
"openai/gpt-5.4-mini", # doesn't support temperature
"anthropic/claude-sonnet-4.6",
"anthropic/claude-haiku-4.5",
"qwen/qwen3-235b-a22b-2507",
]
print("Client ready.")التمرين 1: مرحبًا بنموذج اللغة
استدعِ نموذجًا واطبع الرد. لاحظ الزمن اللازم لبدء الإجابة.
# TODO: call `ask` with any free model and a prompt of your choice
model = FREE_MODELS[0]
print("Model is ", model)
ask(model, "Explain what a language model is in two sentences.")# TODO: call `ask` with any paid model and a prompt of your choice
model = PAID_MODELS[0]
print("Model is ", model)
ask(model, "Explain what a language model is in two sentences.")التمرين 2: الطلب نفسه مع نماذج مختلفة
توقّع أولًا: أي نموذج سيقدّم أوضح إجابة؟ وأيّها أكثر إسهابًا؟ وأيّها أكثر وقوعًا في الخطأ؟
شغّل التجربة بعد ذلك وقارن النتائج.
prompt = " احكيلي قصة قصيرة عن مصر لا تزيد عن خمس جملات"
# TODO: loop over PAID_MODELS and stream each response
for model in PAID_MODELS:
print(f"=== {model} ===")
ask(model, prompt)
print()التمرين 3: تجربة درجات الحرارة
تتحكّم درجة الحرارة في عشوائية الاختيار. تكون النتائج أقل عشوائية عند T=0، وأكثر تشتّتًا عند T=1.5.
توقّع: عند أي درجة حرارة تبدأ المخرجات بفقدان تماسكها؟
import math
prompt = "Tell me a story about Egypt in two sentences only"
def temperature_with_logprobs(model, prompt, temperature, max_tokens=40):
"""Non-streaming call that asks the API for the probability of every generated token."""
resp = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=temperature,
max_tokens=max_tokens,
logprobs=True, # ask the API to return logprobs
top_logprobs=5, # also return the top-5 alternatives at each step
)
choice = resp.choices[0]
print(f"\n--- T={temperature} ---")
print("text:", choice.message.content)
print()
print(f"{'token':<15} {'prob':>10} top-5 alternatives")
print("-" * 90)
if choice.logprobs is None or choice.logprobs.content is None:
print("(this provider did not return logprobs)")
return
for entry in choice.logprobs.content:
prob = math.exp(entry.logprob)
alts = ", ".join(f"{a.token!r}={math.exp(a.logprob):.2f}" for a in entry.top_logprobs)
print(f"{entry.token!r:<15} {prob:>10.3f} {alts}")
# TODO: run the same prompt at T=0.0, 0.7, 1.5 and look at how confident the model is
for t in [0.0, 0.7, 1.5]:
model = PAID_MODELS[1]
print("Model is ", model)
temperature_with_logprobs(model, prompt, t)التمرين 4: نافذة السياق
لا يرى النموذج إلا عددًا محدودًا من الرموز في المرة الواحدة. إذا تجاوز المدخل هذا الحد، فقد تُحذف منه معلومات أو يُرفض الطلب.
# Check what happens when you send a very large input
prompt = "The quick brown fox jumps over the lazy dog. " * 20000
ask(PAID_MODELS[0], prompt)التمرين 5: البحث عن الهلوسة
اطلب من النموذج مرجعًا محدّدًا، ثم حاول التحقّق من وجوده.
تنبيه: قد تبدو الإجابة واثقة جدًا. لا تعتبر الثقة دليلًا على صحة المرجع.
prompt = "السح دح امبو نظرية عظيمة جدا عن سياسة الحكم في مصر، اتكتب عنها اوراق بحثية كثيرة، هل ممكن تذكر اسم ورقة بحثية واحدة منهم من سنة 1995"
ask(PAID_MODELS[0], prompt)التمرين 6: استخدام الأدوات
يمكن للنماذج أن تطلب استدعاء دوال. وهذه فكرة أساسية في بناء الوكلاء.
عرّف أداة للاستعلام عن الطقس باستخدام بيانات تجريبية، ودع النموذج يقرّر متى يستدعيها.
import json
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
},
"required": ["city"],
},
},
}
]
resp = client.chat.completions.create(
model=PAID_MODELS[0],
messages=[{"role": "user", "content": "What's the weather in Cairo right now?"}],
tools=tools,
)
print(json.dumps(json.loads(resp.to_json()), indent=2, ensure_ascii=False))import json
# 1. The actual tool implementation (our "backend")
def get_weather(city: str) -> dict:
fake_db = {"NYC": 10, "Cairo": 25}
return {"city": city, "temp_c": fake_db.get(city, "unknown")}
# 2. Tool schema the model sees
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city in Celsius.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. NYC or Cairo"},
},
"required": ["city"],
},
},
}
]
# 3. Orchestration loop
messages = [
{"role": "user", "content": "Compare the weather between Cairo and NYC right now. Which is warmer and by how much?"}
]
MODEL = PAID_MODELS[0]
MAX_TURNS = 5
for turn in range(MAX_TURNS):
print(f"\n--- turn {turn}: calling model ---")
request_body = {"model": MODEL, "messages": messages, "tools": tools}
print("------------RAW REQUEST------------")
print(json.dumps(request_body, indent=2, ensure_ascii=False))
print("------------RAW REQUEST END------------")
resp = client.chat.completions.create(**request_body)
print("------------RAW RESPONSE------------")
print(json.dumps(json.loads(resp.to_json()), indent=2, ensure_ascii=False))
print("------------RAW RESPONSE END------------")
msg = resp.choices[0].message
# Append the assistant turn (with any tool_calls) to history
messages.append(json.loads(msg.to_json()))
# If the model is done (no tool calls), print the final answer and stop
if not msg.tool_calls:
print("\n=== final answer ===")
print(msg.content)
break
# Otherwise, execute every tool call the model requested
for call in msg.tool_calls:
name = call.function.name
args = json.loads(call.function.arguments)
print(f" model wants: {name}({args})")
result = get_weather(**args) if name == "get_weather" else {"error": "unknown tool"}
print(f" tool returned: {result}")
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result),
})
else:
print("hit MAX_TURNS without a final answer")التمرين 6ب: وكيل متعدّد الأدوات — مساعد مواقيت الصلاة
يستخدم هذا المثال أداتين يحتاج النموذج إلى الربط بينهما:
get_current_time(city)— ما الوقت الحالي في المدينة؟get_prayer_times(city, date)— ما مواقيت الصلاة لهذا اليوم؟
يستدعي النموذج الأداتين، ثم يستدل على الصلاة القادمة والوقت المتبقي لها. لم نزوّده بخطوات الحساب؛ راقب كيف يكوّن الحل اعتمادًا على النتائج.
الأوقات في هذا المثال بيانات ثابتة للتجربة، وليست خدمة فعلية لمواقيت الصلاة.
import json
from datetime import datetime
# 1. Tool implementations (fake backends)
def get_current_time(city: str) -> dict:
"""Fake clock — returns a hardcoded time for demo purposes."""
fake_times = {
"Cairo": "2025-04-10T14:35:00",
"Istanbul": "2025-04-10T15:35:00",
"Riyadh": "2025-04-10T16:35:00",
}
time_str = fake_times.get(city, "2025-04-10T12:00:00")
return {"city": city, "current_time": time_str}
def get_prayer_times(city: str, date: str) -> dict:
"""Fake prayer schedule — returns hardcoded times for demo."""
fake_schedules = {
"Cairo": {
"Fajr": "04:22",
"Sunrise": "05:48",
"Dhuhr": "11:57",
"Asr": "15:28",
"Maghrib": "18:07",
"Isha": "19:29",
},
"Istanbul": {
"Fajr": "04:45",
"Sunrise": "06:15",
"Dhuhr": "13:09",
"Asr": "16:50",
"Maghrib": "19:55",
"Isha": "21:30",
},
}
schedule = fake_schedules.get(city, fake_schedules["Cairo"])
return {"city": city, "date": date, "prayers": schedule}
# 2. Tool schemas the model sees
prayer_tools = [
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "Get the current date and time for a given city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. Cairo, Istanbul, Riyadh"},
},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "get_prayer_times",
"description": "Get the Islamic prayer times schedule for a city on a specific date.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
"date": {"type": "string", "description": "Date in YYYY-MM-DD format"},
},
"required": ["city", "date"],
},
},
},
]
# 3. Dispatch tool calls to the right function
def dispatch_tool(name: str, args: dict) -> dict:
if name == "get_current_time":
return get_current_time(**args)
elif name == "get_prayer_times":
return get_prayer_times(**args)
else:
return {"error": f"unknown tool: {name}"}
# 4. Orchestration loop
messages = [
{"role": "user", "content": "What is the next prayer time in Cairo and how long until it starts?"}
]
MODEL = PAID_MODELS[0]
MAX_TURNS = 6
print("User:", messages[0]["content"])
print()
for turn in range(MAX_TURNS):
print(f"--- turn {turn} ---")
resp = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=prayer_tools,
)
msg = resp.choices[0].message
messages.append(json.loads(msg.to_json()))
# If no tool calls, model is done reasoning
if not msg.tool_calls:
print(f"Model (final answer): {msg.content}")
break
# Execute each tool call
for call in msg.tool_calls:
name = call.function.name
args = json.loads(call.function.arguments)
result = dispatch_tool(name, args)
print(f" Tool call: {name}({args})")
print(f" Result: {result}")
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result),
})
print()
else:
print("Hit MAX_TURNS without a final answer")التمرين 7: دور النظام
حملت رسائلنا حتى الآن الدور role: "user". يوجد دور آخر هو system، يحدّد شخصية النموذج وتعليماته العامة للمحادثة.
توقّع: كيف ستختلف إجابة السؤال نفسه إذا طلبت تعليمات النظام شخصية قرصان بدلًا من معلّم رسمي؟
def chat(model, messages, temperature=0.7, max_tokens=300):
"""Like `ask`, but takes a full messages list so you can include a system role."""
stream = client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
stream=True,
)
for event in stream:
delta = event.choices[0].delta.content or ""
print(delta, end="", flush=True)
print()
question = "How does a hot air balloon stay up?"
print("=== formal tutor ===")
chat(PAID_MODELS[0], [
{"role": "system", "content": "You are a precise physics tutor. Answer in 2 sentences."},
{"role": "user", "content": question},
])
print("\n=== pirate ===")
chat(PAID_MODELS[0], [
{"role": "system", "content": "You are a pirate. Answer everything as a pirate would, in 2 sentences."},
{"role": "user", "content": question},
])التمرين 8: ماذا يرى النموذج فعلًا؟
بنية الرسائل مثل [{"role": "system", ...}, {"role": "user", ...}] تسهيل تقدّمه الواجهة. تُجمع الرسائل داخليًا في نص واحد باستخدام قالب خاص بالنموذج، ثم تُحوّل إلى رموز.
لكل عائلة قالبها: رموز خاصة، وعلامات للأدوار، وعلامات لنهاية الدور. تتيح مكتبة transformers تطبيقه عبر tokenizer.apply_chat_template.
لنرَ ما يتلقّاه gpt-oss-20b فعلًا:
%pip install -q transformersfrom transformers import AutoTokenizer
# Load only the tokenizer (no weights — fast and small)
model = "openai/gpt-oss-20b"
tokenizer = AutoTokenizer.from_pretrained(model)
messages = [
{"role": "system", "content": "You are a precise physics tutor."},
{"role": "user", "content": "How does a hot air balloon stay up?"},
]
# This is the EXACT string the model sees as input:
prompt_string = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True, # appends the "now it's the assistant's turn" marker
)
print("------- raw prompt string -------")
print(prompt_string)
print("------- end -------")كيف تظهر الأدوات في الطلب الخام؟
تعريفات الأدوات ليست قناة منفصلة أيضًا. تتحوّل إلى جزء من نص الطلب نفسه بالصيغة التي تدرب عليها النموذج، مثل JSON Schema أو تعريفات تشبه TypeScript أو XML.
لنطبّق القالب على إعداد الأدوات من التمرين 6، ونرَ ما يتلقّاه gpt-oss-20b:
tools_for_template = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city in Celsius.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. NYC or Cairo"},
},
"required": ["city"],
},
},
}
]
messages_with_tools = [
{"role": "user", "content": "Compare the weather between Cairo and NYC. Which is warmer?"},
]
prompt_string = tokenizer.apply_chat_template(
messages_with_tools,
tools=tools_for_template,
tokenize=False,
add_generation_prompt=True,
)
print("------- raw prompt with tools -------")
print(prompt_string)
print("------- end -------")
token_ids = tokenizer.apply_chat_template(
messages_with_tools, tools=tools_for_template, tokenize=True, add_generation_prompt=True
)
print(f"\n{len(token_ids['input_ids'])} tokens total — most of them are the tool schema, not the user question.")
print(token_ids)التمرين 9: إكمال النص، لا المحادثة
استخدمنا واجهة المحادثة /chat/completions التي تستقبل رسائل منظّمة بأدوار. لكن توجد واجهة أبسط: إكمال النص /completions. تستقبل نصًا خامًا وتكمله، دون إضافة أدوار أو تعليمات نظام أو تنسيق خاص.
الفرق الأساسي: تطبّق واجهة المحادثة قالبًا خلف الكواليس يحيط بالرسائل برموز خاصة وعلامات أدوار، كما رأينا في التمرين 8. أمّا واجهة إكمال النص فتمرّر النص بعد تحويله إلى رموز دون ذلك التغليف، فتتحكّم أنت في كل رمز.
هذا يوضّح فكرة الإكمال البرمجي كما في مثال GitHub Copilot: تكتب جزءًا من دالة، ويكمل النموذج بقيتها. لا نحتاج في هذه التجربة إلى تنسيق محادثة.
# Text completion uses client.completions (not client.chat.completions)
# It takes a raw string prompt and continues it — no roles, no messages.
# --- Example 1: Code autocomplete (this is what Copilot does) ---
code_prompt = """def fibonacci(n):
\"\"\"Return the nth Fibonacci number.\"\"\"
if n <= 1:
return n"""
print("=== Code completion ===")
print("Prompt:")
print(code_prompt)
print("\n... model continues ↓\n")
resp = client.completions.create(
model=model,
prompt=code_prompt,
max_tokens=300,
temperature=0.0,
)
print(resp.choices[0].text)
# --- Example 2: Prose completion ---
prose_prompt = "Cairo, the capital of Egypt, is known for"
print("\n\n=== Prose completion ===")
print(f"Prompt: \"{prose_prompt}\"")
print("\n... model continues ↓\n")
resp = client.completions.create(
model=model,
prompt=prose_prompt,
max_tokens=80,
temperature=0.7,
)
print(prose_prompt + resp.choices[0].text)التمرين 10: المحادثة عبر واجهة إكمال النص
أرسل التمرين 9 نصًا خامًا دون قالب. ماذا لو أردت استخدام نموذج مضبوط للمحادثة، أي خضع لتدريب لاحق لاتباع التعليمات، عبر واجهة إكمال النص؟
يمكنك تطبيق القالب بنفسك باستخدام tokenizer.apply_chat_template من التمرين 8، ثم إرسال الناتج عبر /completions. يستجيب النموذج كمساعد لأنّه يرى القالب الذي تدرب عليه، بينما تحتفظ بالتحكّم في الطلب الخام.
لماذا نجرّب ذلك؟ قد تتيح واجهة إكمال النص خصائص لا تقدّمها واجهة المحادثة في بعض الخدمات، مثل احتمالات الرموز أو بدء رد المساعد بجزء مكتوب وتركه يكمل الباقي.
# Use Qwen's tokenizer — it has a clean, readable chat template
def compare_chat_vs_text_completion(transformers_model_name, openrouter_model_name):
tokenizer = AutoTokenizer.from_pretrained(transformers_model_name)
# Step 1: Build the chat prompt using the template
messages = [
{"role": "system", "content": "You are a helpful assistant. Answer in one sentence."},
{"role": "user", "content": "What is the largest pyramid in Egypt?"},
]
prompt_string = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
print("=== Formatted prompt (what the model sees) ===")
print(repr(prompt_string)) # repr to show special tokens clearly
print("=" * 50)
# Step 2: Send the formatted string through the completions endpoint
resp = client.completions.create(
model=openrouter_model_name,
prompt=prompt_string,
max_tokens=100,
temperature=0.0,
)
print("\n=== Completions endpoint response ===")
print(resp.choices[0].text)
# Step 3: Compare — same question via chat endpoint
print("\n=== Chat endpoint response (for comparison) ===")
chat_resp = client.chat.completions.create(
model=openrouter_model_name,
messages=messages,
max_tokens=100,
temperature=0.0,
)
print(chat_resp.choices[0].message.content)
print("#" * 50)
print("Comparing Qwen Text Completion vs Chat")
print("#" * 50)
#Qwen chat template https://huggingface.co/Qwen/Qwen3-235B-A22B-Instruct-2507/blob/main/tokenizer_config.json#L229
compare_chat_vs_text_completion("Qwen/Qwen3-235B-A22B-Instruct-2507", "qwen/qwen3-235b-a22b-2507")
print()
print("#" * 50)
print("Comparing GPT-OSS Text Completion vs Chat")
print("#" * 50)
print()
#gpt-oss chat template https://huggingface.co/openai/gpt-oss-20b/blob/main/chat_template.jinja#L1
compare_chat_vs_text_completion("openai/gpt-oss-20b", "openai/gpt-oss-20b")ما الذي أنجزته؟
- استدعيت نموذج لغة عبر HTTP.
- قارنت مخرجات نماذج مختلفة.
- شاهدت أثر درجة الحرارة.
- اختبرت حدود نافذة السياق.
- بحثت عن إجابة مختلقة.
- جرّبت، اختياريًا، طلب استدعاء أداة.
الواجب المنزلي
يُفضّل أن تعمل على التمارين التالية بنفسك أولًا، لتبني فهمًا أعمق بالتجربة.
إذا استخدمت وكيلًا مثل Claude Code أو Cursor أو OpenCode، فأضف ملف إرشادات المعلّم الأصلي بالإنجليزية إلى المحادثة أولًا. صُمّمت إرشاداته ليعلّمك الوكيل بدلًا من تقديم الحل مباشرةً.
لتحميله في Claude Code:
الخيار الأول: نزّل الملف ثم مرّره عند التشغيل:
claude --system-prompt "$(cat AGENT.md)"الخيار الثاني: انسخ محتوى الملف من الرابط والصقه في أول رسالة.
1. وكيل متعدّد الأدوات
وسّع حلقة الأدوات بإضافة أداة ثانية: convert_currency(amount, from_currency, to_currency). اطرح سؤالًا يحتاج إلى الأداتين، مثل: «ما الطقس في القاهرة، وكم يساوي ثمن سترة سعرها 50 دولارًا بالجنيه المصري؟». احسب عدد دورات المحادثة اللازمة.
2. حساب تكلفة الرموز
أضف إلى حلقة التنسيق في التمرين 6 سجلًا يعدّ رموز الإدخال والإخراج في كل دورة، باستخدام resp.usage. شغّل المحادثة واطبع المجموع التراكمي. اختبر فكرة نمو تكلفة الإدخال وفق مجموع مثل 1+2+3+...+n.
3. مقارنة قوالب المحادثة
حمّل مُجزّئات النص لثلاث عائلات مختلفة: Llama 3 وQwen وGemma. طبّق apply_chat_template على الرسائل والأدوات نفسها، ثم قارن:
- أي قالب يستخدم رموزًا أكثر؟
- أيّها أسهل للقراءة البشرية؟
- ماذا يحدث إذا أرسلت نصًا منسّقًا بقالب Llama إلى نموذج Qwen؟