در این فصل چه یاد میگیری#
چهار ترم مدل ساختی و آموزش دادی. این ترم چیزِ دیگری است: مدل را دیگری ساخته و تو باید دورش یک سامانه بسازی که واقعاً کار کند — پاسخِ نامعتبر بگیرد و نشکند، سرویس که پایین آمد جا نزند، و وقتی بد جواب داد بفهمی چقدر بد.
در این فصل اولین تماس را میگیری، پارامترهای تولید را دستکاری میکنی، و مهمتر از همه: کدی مینویسی که وقتی سرویس خطا میدهد از پا نمیافتد. آخرش با عدد نشان میدهیم که همان چند خط، نرخِ موفقیت را از ۱۰ از ۲۰ به ۱۹ از ۲۰ میرساند — و میگوییم بهایش چه بود.

آخر این فصل میتوانی:
- پیامها را با
roleدرست بسازی و بفرستی temperatureو سقفِ طولِ خروجی را بشناسی و اثرشان را اندازه بگیری- خطاهای واقعیِ یک سرویس را دستهبندی کنی و برایشان
retryباbackoffبنویسی - بگویی چرا
retryرایگان نیست
قبل از شروع#
از سرنخ ترمِ ۴: فراخوانیِ یک API با کلید و گذاشتنِ کلید در Colab Secrets. اینجا همان کار را میکنیم، ولی این بار کدِ اطرافش موضوعِ درس است، نه خودِ تماس.
از سرنخ ترمِ ۱ فصل ۸: خواندنِ traceback و گرفتنِ استثنا با try/except.
سلولِ راهاندازی سه چیز میدهد: DOCS (دوازده سندِ فارسیِ کتابخانهٔ ساختگیِ «نوردانه»)، GOLD (بیستوچهار پرسش با جوابِ درست، از فصلِ ۶ به بعد ستونِ فقراتِ ترم)، و دو تابعِ تماس: chat و real_chat.
📓 نوتبوک: نوتبوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و بهترتیب داخلش هست.
۱. دو تابعِ تماس، و چرا دو تا#
real_chat تماسِ واقعی است با Gemini، با کلیدِ خودت. chat بدلِ محلیِ آن است — همان ورودی، همان شکلِ خروجی، ولی بدونِ اینترنت و بدونِ کلید، و قطعی: هر بار همان جواب.
چرا این ترم روی بدل بنا شده، صریح میگویم:
- هر عددی که در این کتاب میبینی باید برای تو هم دقیقاً همان دربیاید. با یک سرویسِ زنده این ممکن نیست؛ خروجیاش هر هفته عوض میشود.
- درسِ این ترم مدل نیست، سامانهٔ دورِ مدل است. اعتبارسنجی،
retry، بازیابی،eval، هزینه، ایمنی — هیچکدام به هوشِ مدل وابسته نیستند. - این خودش روشِ حرفهای است. هیچ تیمی تستِ خودکارش را به یک سرویسِ پولیِ غیرقطعی وصل نمیکند. بدل داشتن قاعده است، نه استثنا.
و مرزش را هم صریح میگویم: بدلِ ما فقط استخراج میکند — جملهٔ مرتبط را از متنی که بهش دادهای برمیدارد. استدلال نمیکند. پس هر جا در این ترم عددی میبینی که بدل بد گرفته، مدلِ واقعی ممکن است بهتر بگیرد — و هر جا بدل خوب گرفته، مدلِ واقعی هم میگیرد. این را همهجا یادآوری میکنم.
۲. یک درخواست واقعاً چیست#
import json
messages = [
{"role": "system", "content": "تو دستیارِ کتابخانهٔ نوردانهای. فقط از متنی که میدهم جواب بده."},
{"role": "user", "content": DOC_BY_ID["hours"]["text"] + "\n\nپنجشنبهها ساعت کار کتابخانه تا چند است؟"},
]
for m in messages:
print(f'{m["role"]:>6} | {m["content"][:52]}…')
system | تو دستیارِ کتابخانهٔ نوردانهای. فقط از متنی که مید…
user | کتابخانه شنبه تا چهارشنبه از ساعت ۸ تا ۲۰ باز است. پ…
یک فهرست از دیکشنری. همین. هر چیزِ دیگری که دربارهٔ «چت با هوش مصنوعی» شنیدهای، رویِ همین ساختار سوار است.
سه role رایج است:
system— دستورِ پایدار: تو کی هستی، چه کاری بکن، چه کاری نکن.user— چیزی که کاربر میگوید، بهعلاوهٔ هر متنی که تو بهش میچسبانی.assistant— پاسخهای قبلیِ خودِ مدل. حافظهٔ گفتوگو همین است و بس: مدل چیزی بهخاطر نمیسپارد؛ تو تاریخچه را هر بار دوباره میفرستی.
آن جملهٔ آخر را دستکم نگیر. مدل بینِ دو تماس هیچ حالتی نگه نمیدارد. اگر گفتوگوی دهمرحلهای میخواهی، در مرحلهٔ دهم داری هر ده پیام را دوباره میفرستی — و برای هر دهتا دوباره پول میدهی. فصلِ ۸ همین را اندازه میگیرد.
و متنِ سند را کجا گذاشتیم؟ داخلِ پیامِ user. فعلاً با دست. فصلِ ۴ همین کار را خودکار میکند و اسمش RAG است.
۳. اولین تماس#
reply = chat(messages)
print("متن :", reply["text"])
print("پایان:", reply["finish"])
print("مصرف :", reply["usage"])
متن : پنجشنبهها ساعت کار از ۸ تا ۱۴ است.
پایان: stop
مصرف : {'input_chars': 316, 'output_chars': 35}
✅ چک کن: اگر جوابی گرفتی که ربطی به پنجشنبه ندارد، احتمالاً پرسش را اولِ پیام گذاشتهای. قرارِ این ترم این است که پرسش آخرین سطرِ پیامِ کاربر باشد و سند بالایش. این فقط قاعدهٔ بدلِ ما نیست؛ توصیهٔ عملیِ کارِ با مدلِ واقعی هم هست — دستورِ نزدیک به انتهای متن بیشتر رعایت میشود.
سه فیلد را نگاه کن:
text— چیزی که میخواستی.finish— چرا تولید تمام شد.stopیعنی مدل خودش تمام کرد. مقدارِ دیگری هم دارد که در بخشِ ۵ میبینی.usage— مصرف. سرویسِ واقعی اینجا توکن میدهد؛ بدلِ ما نویسه میشمارد چون توکنایزرِ آن مدل را ندارد. در فصلِ ۸ با توکنایزرِ واقعی دقیق میشماریم.
۴. همان تماس، این بار واقعی#
try:
real = real_chat(messages)
print("پاسخِ مدلِ واقعی:", real["text"])
print("مصرفِ واقعی:", real["usage"])
except LLMError as error:
print("تماسِ واقعی انجام نشد →", error)
تماسِ واقعی انجام نشد → کلید پیدا نشد: GOOGLE_API_KEY را در Colab Secrets بگذار و دوباره اجرا کن.
اگر کلید گذاشتهای، این سلول جوابِ واقعی میدهد و طبیعتاً با متنِ بالا فرق دارد. بقیهٔ فصلها روی بدل کار میکنند تا عددهایت با کتاب یکی باشد.
برای گرفتن و گذاشتنِ کلید:
- ۱) از
aistudio.google.comیک کلیدِ رایگان بساز. - ۲) در Colab، از نوارِ کناری 🔑 را باز کن، نامش را
GOOGLE_API_KEYبگذار و مقدارش را بچسبان، و «دسترسیِ نوتبوک» را روشن کن. - ۳) در نوتبوک این دو خط را اجرا کن:
from google.colab import userdata
os.environ["GOOGLE_API_KEY"] = userdata.get("GOOGLE_API_KEY")
🔧 اگر کار نکرد:
SecretNotFoundErrorیعنی نامِ رمز با چیزی که صدا میزنی یکی نیست — نام حساس به بزرگی و کوچکیِ حروف است.NotebookAccessErrorیعنی رمز هست ولی دسترسیِ همین نوتبوک را روشن نکردهای. و هرگز کلید را مستقیم داخلِ سلول ننویس — نوتبوک را که به اشتراک بگذاری، کلیدت هم میرود.
داخلِ real_chat یک نکتهٔ مهندسی هست که ارزشِ دیدن دارد: قالبِ پیامِ Gemini با قالبِ role/content که نوشتیم یکی نیست — آن contents و parts میخواهد و دستورِ سامانه را جدا میگیرد. پس real_chat مترجم است. این الگو را نگه دار: کدت را با قالبِ خودت بنویس و ترجمه به هر سرویس را در یک تابع حبس کن. آن روز که سرویس را عوض میکنی، فقط همان یک تابع عوض میشود.
۵. temperature: چقدر جسور#
reset_calls()
cold = {chat(messages, temperature=0.0)["text"] for _ in range(8)}
reset_calls()
warm = {chat(messages, temperature=1.0, seed=3)["text"] for _ in range(8)}
print("temperature=0.0 →", len(cold), "پاسخِ متمایز در ۸ تماس")
print("temperature=1.0 →", len(warm), "پاسخِ متمایز در ۸ تماس")
for text in sorted(warm):
print(" ", text[:54])
temperature=0.0 → 1 پاسخِ متمایز در ۸ تماس
temperature=1.0 → 4 پاسخِ متمایز در ۸ تماس
باجهٔ امانت نیمساعت زودتر از پایانِ ساعتِ کار بسته می
جمعهها کتابخانه تعطیل است.
پنجشنبهها ساعت کار از ۸ تا ۱۴ است.
کتابخانه شنبه تا چهارشنبه از ساعت ۸ تا ۲۰ باز است.
در ترمِ ۴ فصلِ ۷ temperature را روی مدلِ خودت دیدی: تقسیمِ logits بر یک عدد، قبل از softmax. همان است، فقط این بار داخلِ سرویسِ دیگری.
و نتیجهاش اینجا هم همان: ۰ یعنی همیشه محتملترین، بالاتر یعنی گاهی گزینههای بعدی. سه پاسخِ اضافهای که بالا میبینی همگی از همان سند آمدهاند و همگی غلطاند — چون پرسش دربارهٔ پنجشنبه بود.
قاعدهٔ عملی: هر جا خروجی باید درست باشد — استخراج، دستهبندی، JSON، جوابِ روی سند — temperature=0. هر جا تنوع ارزش دارد — طوفانِ فکری، چند عنوانِ جایگزین — بالاتر ببر. پیشفرضِ اکثرِ سرویسها صفر نیست، پس اگر خروجیِ برنامهات بیدلیل بیثبات است، اول اینجا را نگاه کن.
📏 اندازه بگیر: «صفر یعنی قطعی» یک تقریبِ مفید است، نه یک تضمین. روی سرویسِ واقعی حتی با
temperature=0هم گاهی خروجی فرق میکند (سختافزار، بهروزرسانیِ مدل،batchشدنِ درخواستها). پس اگر برنامهات به یکسان بودنِ خروجی وابسته است، وابستگی را بردار — بهtemperatureتکیه نکن.
۶. سقفِ طولِ خروجی#
short = chat(messages, max_output_tokens=4)
print("متن :", short["text"])
print("پایان:", short["finish"])
متن : پنجشنبهها ساعت
پایان: length
finish شد length و جمله وسطِ راه بریده شد.
این بیسروصداترین خرابیِ کارِ با مدل است. خروجی میآید، خطایی نمیگیری، برنامه ادامه میدهد — و متن ناقص است. اگر خروجی JSON بوده باشد، قالبش میشکند و فصلِ بعد دقیقاً همین را نشان میدهد.
قاعده: همیشه finish را چک کن، نه فقط text را. یک if reply["finish"] != "stop" سهخطی، ساعتها اشکالیابی را میخرد.
۷. وقتی سرویس جواب نمیدهد#
FAILURES["rate"] = 0.4
reset_calls()
def try_once():
try:
chat(messages)
return "ok"
except LLMError as error:
return type(error).__name__
results = [try_once() for _ in range(20)]
print("موفق:", results.count("ok"), "از ۲۰")
print("شکستها:", {k: results.count(k) for k in sorted(set(results)) if k != "ok"})
موفق: 10 از ۲۰
شکستها: {'LLMTimeout': 1, 'RateLimitError': 5, 'ServerError': 4}
FAILURES["rate"] شیرِ خرابیِ بدل است: با ۰٫۴ حدودِ چهل درصدِ تماسها شکست میخورند — قطعی و تکرارپذیر، تا بتوانی راهحلت را واقعاً بسنجی.
سه خطا سه معنیِ کاملاً متفاوت دارند و یکسان با آنها رفتار کردن اشتباه است:
| خطا | کدِ HTTP | یعنی | کارِ درست |
|---|---|---|---|
RateLimitError |
۴۲۹ | از سهمیه گذشتی | صبر کن و دوباره بزن |
ServerError |
۵۰۰ و بالاتر | مشکل سمتِ آنهاست | صبر کن و دوباره بزن |
LLMTimeout |
— | پاسخ بهموقع نرسید | دوباره بزن، ولی حواست به تماسِ نیمهکاره باشد |
| خطای کلید | ۴۰۱ و ۴۰۳ | کلید غلط یا بیاجازه است | هرگز دوباره نزن — تا کلید عوض نشود همان جواب میآید |
| ورودیِ نامعتبر | ۴۰۰ | درخواستت خراب است | هرگز دوباره نزن — خودت را درست کن |
دو ردیفِ آخر را جدی بگیر. retry روی خطای ۴۰۰ و ۴۰۱ فقط سهمیهات را میسوزاند و ممکن است حسابت را موقتاً ببندد. فقط روی خطاهای گذرا دوباره تلاش کن.
۸. retry با backoff#
import time
SLEEP_SCALE = 0.0 # در Colab بگذار 1.0 تا صبرِ واقعی را حس کنی
def with_retry(call, attempts=4, base=0.5, verbose=False):
for i in range(attempts):
try:
return call()
except (RateLimitError, ServerError, LLMTimeout) as error:
if i == attempts - 1:
raise
wait = base * (2 ** i)
if verbose:
print(f" تلاشِ {i + 1}: {type(error).__name__} — {wait:.1f} ثانیه صبر")
time.sleep(wait * SLEEP_SCALE)
raise LLMError("unreachable")
reset_calls()
print("یک تماس با گزارشِ کامل:")
with_retry(lambda: chat(messages), verbose=True)
print(" گرفتیم.")
یک تماس با گزارشِ کامل:
تلاشِ 1: RateLimitError — 0.5 ثانیه صبر
تلاشِ 2: ServerError — 1.0 ثانیه صبر
گرفتیم.
دو بار شکست، بارِ سوم گرفت. و مهمتر از خودِ تکرار، فاصلهٔ بینِ تلاشهاست: نیم ثانیه، یک ثانیه، دو ثانیه — هر بار دو برابر. اسمش exponential backoff است.
چرا دو برابر و نه ثابت؟ چون خطای ۴۲۹ یعنی سرویس شلوغ است. اگر همه بلافاصله دوباره بزنند، شلوغی بیشتر میشود — و این دقیقاً همان چیزی است که یک اتفاقِ کوچک را به یک قطعیِ بزرگ تبدیل میکند. عقبنشینیِ نمایی به سرویس فرصتِ نفس کشیدن میدهد.
💡 نکته: در کارِ واقعی یک تکهٔ تصادفیِ کوچک هم به
waitاضافه میکنند (jitter). دلیلش این است که اگر هزار برنامه همزمان خطا بگیرند، همه دقیقاً نیم ثانیهٔ بعد با هم برمیگردند و موجِ دوم را میسازند. یکrandom.uniform(0, wait)این موج را پخش میکند.
۹. حالا اندازه بگیریم#
reset_calls()
ok = 0
for _ in range(20):
try:
with_retry(lambda: chat(messages))
ok += 1
except LLMError:
pass
print("با with_retry:", ok, "از ۲۰ تماس به جواب رسید")
print("شمارِ کلِ تماسهای واقعیِ زدهشده:", _CALLS["n"])
FAILURES["rate"] = 0.0
با with_retry: 19 از ۲۰ تماس به جواب رسید
شمارِ کلِ تماسهای واقعیِ زدهشده: 40
۱۰ از ۲۰ شد ۱۹ از ۲۰ — با ده خط کد.
📏 اندازه بگیر: و بهایش را هم بخوان: ۴۰ تماسِ واقعی برای ۲۰ درخواست. یعنی هزینه و بارِ روی سرویس دو برابر شد.
retryرایگان نیست؛ قابلیتِ اطمینان را با پول و تأخیر میخری. و آن یک موردی که با چهار تلاش هم نگرفت، یادآوریِ این است که همیشه باید یک مسیرِ شکستِ محترمانه داشته باشی — پیامی به کاربر، نه یکtraceback.
۱۰. streaming: پاسخ تکهتکه#
def real_stream(messages, model=None, timeout=60):
"""همان تماس، ولی پاسخ تکهتکه میرسد. با کلیدِ خودت اجرایش کن."""
import os
import requests
key = os.environ.get("GOOGLE_API_KEY")
if not key:
raise LLMError("کلید پیدا نشد: GOOGLE_API_KEY را در Colab Secrets بگذار.")
url = GEMINI_URL.format(model=model or GEMINI_MODEL).replace(
":generateContent", ":streamGenerateContent") + "?alt=sse"
payload = {"contents": [{"role": "user", "parts": [{"text": m["content"]}]}
for m in messages if m["role"] == "user"]}
with requests.post(url, headers={"x-goog-api-key": key}, json=payload,
stream=True, timeout=timeout) as resp:
resp.raise_for_status()
for line in resp.iter_lines(decode_unicode=True):
if line.startswith("data: "):
chunk = json.loads(line[len("data: "):])
for part in chunk["candidates"][0]["content"].get("parts", []):
yield part.get("text", "")
print("real_stream تعریف شد — با کلیدِ خودت اینطور صدایش بزن:")
print(' for piece in real_stream(messages): print(piece, end="")')
real_stream تعریف شد — با کلیدِ خودت اینطور صدایش بزن:
for piece in real_stream(messages): print(piece, end="")
streaming یعنی بهجای یک پاسخِ کامل، تکهتکه بگیری — همان چیزی که در رابطهای گفتوگو میبینی و حروف یکییکی ظاهر میشوند.
و دقیقاً بدان چه چیزی را عوض میکند و چه چیزی را نه:
- تأخیرِ تا اولین نویسه بهشدت کم میشود. کاربر زودتر چیزی میبیند.
- زمانِ کلِ تولید تقریباً همان است. سریعتر نشد، فقط زودتر شروع شد.
- هزینه مو نمیزند. همان توکنها، همان پول.
- و کارِ تو سختتر میشود: تا تکهٔ آخر نرسیده، خروجیِ کامل را نداری. پس هر جا باید JSON را اعتبارسنجی کنی یا ابزاری را صدا بزنی،
streamingبه دردت نمیخورد — اول همهاش را بگیر.
واژههای تازهٔ این فصل#
| کلمه | تلفظ به حروف فارسی | یعنی چه |
|---|---|---|
| role | رول | نقشِ هر پیام: system، user یا assistant |
| temperature | تمپرچر | میزانِ جسارتِ مدل در انتخابِ توکنِ بعدی |
| retry | ریترای | تلاشِ دوباره بعد از یک خطای گذرا |
| exponential backoff | اکسپوننشال بکآف | دو برابر شدنِ زمانِ صبر در هر تلاش |
| jitter | جیتر | تصادفیسازیِ کوچکِ زمانِ صبر تا همه با هم برنگردند |
| rate limit | ریت لیمیت | سقفِ تعدادِ درخواست در واحدِ زمان |
| streaming | استریمینگ | گرفتنِ پاسخ بهصورتِ تکهتکه |
🤖 از دستیارت بپرس: «تفاوتِ
429با529چیست و برای هر کدام چه رفتاری درست است؟» بعد این را بپرس: «اگر سرویس در هدرِ پاسخRetry-Afterبفرستد،backoffنمایی را باید کنار بگذارم؟» — جوابِ درست «بله» است و دلیلش این است که سرویس بهتر از تو میداند کِی آماده میشود.
تمرینها
اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.
در فصل بعد#
تا اینجا خروجی متن بود و خودت خواندیاش. ولی برنامهات نمیتواند متن بخواند — به dict نیاز دارد، با کلیدهای مشخص و مقدارهای از نوعِ درست. فصلِ بعد از مدل JSON میخواهد، میبیند که در ۱۶ بار از ۴۰ بار چیزی میدهد که json.loads رویش میشکند — و یک حلقهٔ تصحیح میسازد که این عدد را به صفر میرساند.
به آخر این فصل رسیدی!
اگر ساختی و جواب داد، این دکمه مال توست.