تصویر شاخص نوشته: وصل‌کردن یک مدل زبانی به آردوینو و ESP32: معماری، ایمنی و هزینه

وصل‌کردن یک مدل زبانی به آردوینو و ESP32: معماری، ایمنی و هزینه

۱۲ مرداد ۱۴۰۵ مبانی هوش مصنوعی

دو صحنه. صحنهٔ اول: پروژه‌ای ساخته‌ای که کار می‌کند — رطوبتِ خاک را می‌خواند، عدد را روی نمایشگر می‌نویسد — و حالا دلت می‌خواهد به‌جای «۴۲٪» یک جملهٔ فارسیِ درست بنویسد: «سه روز است آب ندادی و دما هم بالا رفته.» صحنهٔ دوم: جست‌وجو کرده‌ای «اجرای ChatGPT روی آردوینو» و به دو دسته جوابِ متضاد رسیده‌ای — یکی می‌گوید «شدنی است، این هم کد»، یکی می‌گوید «محال است».

هر دو نصفِ حقیقت را می‌گویند. جواب با عدد به‌دست می‌آید، نه با نظر.

چرا برد نمی‌تواند مدل را اجرا کند

یک مدلِ زبانی از «پارامتر» ساخته شده: عددهایی که در آموزش تنظیم شده‌اند و کلِ دانشِ مدل در آن‌هاست. برای کوچک‌کردنِ فایل فشرده‌شان می‌کنند (quantization)، و فشرده‌ترین حالتِ رایجِ هنوز قابلِ استفاده چهار بیت برای هر پارامتر است. این را کنارِ مشخصاتِ رسمیِ بردها بگذار:

ESP32-WROOM-32 :  520 KB SRAM  +  448 KB ROM  +  4 MB external SPI flash
Arduino Uno    :    2 KB SRAM  +   32 KB flash  +  1 KB EEPROM

یک مدلِ کوچکِ ۱ میلیارد پارامتری، ۴ بیت برای هر پارامتر:
1,000,000,000 × 4 bit = 4,000,000,000 bit = 500,000,000 byte = 500 MB

500 MB ÷ 520 KB (SRAM روی ESP32)   ≈    960 برابر
500 MB ÷   4 MB (فلشِ ESP32)        ≈    125 برابر
500 MB ÷  32 KB (فلشِ Arduino Uno)  ≈ 15,600 برابر

آن ۵۰۰ مگابایت کفِ ماجراست، نه سقفش، و این «کمی جا کم آوردیم» نیست: حافظهٔ برد باید نزدیک به هزار برابر بزرگ‌تر باشد. حتی اگر جا می‌شد هم اجرا نمی‌شد — تولیدِ هر token یعنی یک بار عبورِ کلِ آن عددها از پردازنده، و پردازندهٔ ESP32 دو هستهٔ ۲۴۰ مگاهرتزی است. پس اینجا بحثِ آردوینو یا ESP32 بحثِ حافظه نیست؛ تنها تفاوتی که به کارِ ما می‌آید این است که یکی WiFi دارد و دیگری ندارد.

پس آن ویدیوهای «هوش مصنوعی روی میکروکنترلر» چه‌اند؟

واقعی‌اند، و کارِ متفاوتی می‌کنند: یک شبکهٔ عصبیِ کوچک و آموزش‌دیده برای یک کارِ مشخص روی همین بردها اجرا می‌شود. نمونهٔ رسمیِ تشخیصِ کلمهٔ بیدارباش در TensorFlow Lite Micro مدلی زیرِ ۲۰ کیلوبایت است که فقط «yes» و «no» را می‌شناسد؛ تشخیصِ حرکت از روی شتاب‌سنج و دسته‌بندیِ «عادی / غیرعادی» هم از همین جنس‌اند. ولی خطِ فارق را واضح بکشیم، چون تقریباً همیشه همین‌جا اشتباه می‌شود: این‌ها مدلِ زبانی نیستند. چنین دسته‌بندی از میانِ دو گزینه یکی را برمی‌دارد؛ زبان بلد نیست، جمله نمی‌فهمد، جمله هم نمی‌سازد. اگر بگویی «چراغ را کم‌نور کن»، تنها چیزی که می‌تواند بگوید این است که «نه yes بود و نه no». آنچه از یک جملهٔ آزادِ فارسی معنا بیرون می‌کشد از جنسِ دیگری است، و همان است که ۵۰۰ مگابایت جا می‌خواهد (مدل‌های زبانی چطور کار می‌کنند؟).

معماریِ واقعی: برد دست و حس است، مدل زبان

حسگر / موتور  ⇄  برد (ESP32)  ⇄  WiFi  ⇄  اینترنت  ⇄  API مدل
   دنیای فیزیکی      کدِ تو، بلادرنگ                    زبان

برد جهانِ فیزیکی را می‌خواند و تغییر می‌دهد؛ سریع است، بدونِ اینترنت هم کار می‌کند، و زبان نمی‌فهمد. کدِ خودت روی برد تصمیم‌های قطعی و فوری را می‌گیرد — این عدد از حد گذشت، پس موتور خاموش — و هرگز منتظرِ اینترنت نمی‌ماند. مدل زبان می‌فهمد و تولید می‌کند؛ کُند است، پول دارد، و ممکن است اشتباه بگوید.

و نکته‌ای که باید صریح گفته شود: هر فراخوانیِ مدل از ساختمان بیرون می‌رود. داده از برد، از مودم، از اینترنت و از سرورِ شرکتی دیگر می‌گذرد و برمی‌گردد — یعنی بدونِ اینترنت هیچ اتفاقی نمی‌افتد، رفت‌وبرگشت زمان می‌بَرد، و هرچه فرستادی از دستت خارج شده. داده‌ای که نباید بیرون برود، روی خودِ برد به عددِ خلاصه تبدیل می‌شود.

سه الگویی که واقعاً ساخته می‌شوند

الگوی اول: برد → مدل (برد داده می‌دهد، مدل حرف می‌زند)

داده یک‌طرفه است: برد اندازه‌گیری می‌فرستد، مدل متنِ فارسی برمی‌گرداند، و آن متن فقط نمایش داده می‌شود. مثال: گلدانی با حسگرِ رطوبتِ خاک، که برد هر ربع‌ساعت می‌خواندش و شبی یک بار می‌فرستد:

رطوبتِ خاک در ۲۴ ساعتِ گذشته (درصد، هر دو ساعت یک عدد):
38, 33, 27, 23, 20, 18, 17, 16
دمای فعلی: 31 — آخرین آبیاری: 3 روز پیش

در یک جملهٔ فارسیِ کوتاه بگو وضعِ گلدان چطور است. عدد اضافه نساز.

جواب چیزی در این مایه‌هاست: «سه روز است آب ندادی، رطوبت پیوسته پایین آمده و دما هم بالا رفته — امروز آب بده.»

خوب است برای خلاصه‌کردنِ یک مشت عدد به یک جملهٔ قابلِ فهم، توضیحِ وضعیتِ غیرعادی، و ترجمهٔ پیامِ خطای یک کتابخانه به زبانِ آدمیزاد؛ بدترین اتفاقِ ممکن اینجا یک جملهٔ بی‌ربط است. می‌شکند وقتی از مدل بخواهی عدد بسازد: اگر بپرسی «رطوبت چند درصد افت کرده؟»، ممکن است با اطمینانِ کامل عددی بگوید که از هیچ‌جای داده‌ات درنمی‌آید. حساب را کدِ خودت بکند و مدل فقط همان عددِ حساب‌شده را در جمله بنشاند: حساب با کد، جمله با مدل.

الگوی دوم: مدل → برد (جمله وارد می‌شود، فرمانِ ساختاریافته بیرون می‌آید)

جالب‌ترین الگو همین است: آدم یک جملهٔ آزاد می‌گوید و مدل آن را به فرمانی ساختاریافته ترجمه می‌کند که برد می‌تواند اجرایش کند. خروجی نباید متنِ آزاد باشد؛ باید JSON باشد، قالبی ثابت با کلیدهای مشخص:

تو یک مترجمِ فرمانی. جملهٔ کاربر را به یک شیء JSON تبدیل کن و هیچ چیزِ دیگری ننویس.

دستگاه‌های مجاز و بازهٔ هرکدام:
  servo   -> کلید "angle" ، عددِ صحیح بین 0 تا 180
  led     -> کلید "level" ، عددِ صحیح بین 0 تا 255
  buzzer  -> کلید "ms"    ، عددِ صحیح بین 0 تا 2000

اگر جمله به هیچ‌کدام مربوط نبود، دقیقاً این را بنویس: {"device":"none"}

جملهٔ کاربر: «دستِ ربات را تا نصفه بالا ببر»

خروجیِ موردِ انتظار: {"device":"servo","angle":90}. سه چیز اینجا عمدی است: فهرستِ دستگاه‌ها بسته است، بازهٔ هر عدد نوشته شده، و برای حالتِ «نمی‌دانم» خروجیِ مشخصی تعریف شده تا مدل مجبور نشود چیزی از خودش دربیاورد (پرامپت‌نویسی).

خوب است برای فرمانِ صوتی یا متنی به زبانِ طبیعی، جایی که منوی دکمه‌ای برای همهٔ حالت‌ها بی‌معناست: «کمی روشن‌ترش کن» یا «نصفه ببندش» را با if نمی‌شود پوشش داد. می‌شکند همین‌جا، و بد هم می‌شکند: مدل قول نداده JSON تحویل بدهد، فقط متنی می‌سازد که احتمالاً شبیهِ JSON است — ممکن است دورش سه‌تا ` بگذارد، جمله‌ای توضیحی جلویش بنویسد، "angle": 900 بدهد، یا کلیدی بسازد که تو تعریفش نکرده‌ای. بعضی سرویس‌ها حالتی دارند که خروجی را به قالبی مشخص مقید می‌کند؛ استفاده‌اش کن، ولی جای بررسی را نمی‌گیرد.

الگوی سوم: مدل در حلقهٔ تصمیم، هرگز در حلقهٔ کنترل

مدل تصمیم می‌گیرد «چه کاری»؛ کدِ خودت تصمیم می‌گیرد «چطور».

رباتی داری که در اتاق می‌گردد. مدل می‌تواند تصمیم بگیرد «حالا برو سمتِ آشپزخانه» — تصمیمی سطحِ بالا، جایی که فهمِ زبان و زمینه به کار می‌آید. ولی اینکه در این لحظه چرخِ راست چقدر بچرخد تا ربات به دیوار نخورد هرگز نباید از مدل بیاید؛ آن یک حلقهٔ کنترل است، هر چند میلی‌ثانیه یک بار، بر اساسِ عددِ همین لحظه. مرزِ عملی: هر چیزی که باید سریع باشد یا اگر غلط باشد چیزی می‌شکند مالِ کدِ خودت است؛ هر چیزی که زبان است یا انتخابِ سطحِ بالا میانِ چند گزینهٔ همه‌مجاز مالِ مدل.

ایمنی: متن اشتباه است، سخت‌افزار واقعی

مدلِ زبانی متن تولید می‌کند، و متن می‌تواند با اطمینانِ کامل غلط باشد؛ این خاصیتِ کارش است، نه خرابی‌اش — توهم در هوش مصنوعی همین را باز کرده: یک جوابِ درست و یک جوابِ غلط از بیرون دقیقاً یک شکل‌اند. تا وقتی این متن روی صفحه چاپ می‌شود، هزینهٔ اشتباه یک جملهٔ بی‌ربط است؛ وقتی به موتور می‌رسد، عوض می‌شود.

یک servo معمولی حدودِ ۰ تا ۱۸۰ درجه گردش دارد و در دو سرِ این بازه گیرِ مکانیکی است. عددِ ۹۰۰ را که بدهی، به گیر فشار می‌آورد و همان‌جا می‌ماند: ویز می‌کند، جریانِ زیادی می‌کشد، گرم می‌شود، و چرخ‌دنده‌های پلاستیکی‌اش ساییده می‌شوند. همان جریانِ زیاد می‌تواند ولتاژ را پایین بکشد و کلِ برد را ری‌استارت کند — یعنی هر محافظتی که در کد گذاشته بودی همان لحظه از دور خارج می‌شود (آشنایی با سروو موتور). پس اینجا انضباط لازم است، نه خوش‌بینی — پنج قاعده، به‌ترتیبِ اهمیت.

۱. خروجیِ مدل هرگز اجرا نمی‌شود. نه exec، نه eval، نه «کد بنویس تا اجرایش کنم». معنایش این است که هر متنی که از شبکه برگردد اختیارِ کاملِ بردت را دارد، و لازم هم نیست کسی سوءقصد کرده باشد. خروجیِ مدل داده است، نه دستور.

۲. فقط یک فهرستِ بستهٔ فرمان‌ها پذیرفته می‌شود. فهرستِ سفید بنویس: این چند دستگاه، این چند کلید، همین. هر چیزِ دیگری رد می‌شود — نه «حدس بزن چه می‌خواسته»، نه «شبیه‌ترین را بردار» — و همین سؤالِ «اگر مدل چیزِ عجیبی برگرداند چه می‌شود؟» را برای همیشه می‌بندد. فهرستِ سیاه جایش را نمی‌گیرد: اگر چیزهای بد را فهرست کنی همیشه یکی جا می‌ماند.

۳. هر عدد به بازهٔ فیزیکیِ سخت‌افزار چسبانده می‌شود. به این کار clamp می‌گویند و یک خط است: angle = max(0, min(180, angle)) — از داخل به بیرون، min نمی‌گذارد از ۱۸۰ بالاتر برود و max نمی‌گذارد از صفر پایین‌تر بیاید؛ ۹۰۰ می‌شود ۱۸۰، و منفیِ ۴۵ می‌شود صفر. بدونِ این خط عددِ ۹۰۰ مستقیم می‌رود روی موتور و همان اتفاقی می‌افتد که بالا گفتیم. این خط مستقل از مدل کار می‌کند — چه عدد از مدل آمده باشد، چه از حسگری خراب — پس جایش درست پیش از فرمانِ سخت‌افزار است، نه لای کدِ پردازشِ جواب.

۴. اول بررسی، بعد عمل. پیش از هر حرکتی: اصلاً JSON معتبری هست؟ شیء است یا رشته؟ کلیدها همان‌هایی‌اند که انتظار داشتی؟ مقدار عدد است یا رشته‌ای که شبیهِ عدد است؟ و مهم‌تر از خودِ بررسی، برای حالتِ رد شدن باید رفتاری تعریف‌شده داشته باشی — یک شاخهٔ صریح در کد، نه «معمولاً پیش نمی‌آید».

۵. پیش‌فرض «هیچ کاری نکن» است. JSON خراب بود، حرکتی نکن؛ دستگاه در فهرست نبود، حرکتی نکن؛ اینترنت قطع شد، حرکتی نکن. ولی «هیچ کاری نکن» یعنی چیزِ تازه‌ای شروع نشود و کاری که در جریان است ادامه پیدا نکند — موتوری که فرمانِ توقف نگیرد همان کارِ قبلی را ادامه می‌دهد، پس در کد معمولاً یعنی صدا زدنِ یک تابعِ توقف، نه صدا نزدنِ هیچ تابعی.

همهٔ این‌ها در یک قطعه کد

import json

# فهرستِ سفید: فقط این دستگاه‌ها، با همین کلیدها و همین بازه‌ها.
ALLOWED = {
    "servo":  {"key": "angle", "min": 0, "max": 180},
    "led":    {"key": "level", "min": 0, "max": 255},
    "buzzer": {"key": "ms",    "min": 0, "max": 2000},
}


def parse_command(text):
    """متنِ خامِ مدل را می‌گیرد؛ یا فرمانی معتبر برمی‌گرداند، یا None."""

    # ۱) اصلاً JSON هست؟ اگر نبود، همین‌جا تمام.
    try:
        data = json.loads(text)
    except (ValueError, TypeError):
        return None

    # ۲) باید یک شیء باشد؛ لیست و رشته و عدد قبول نیست.
    if not isinstance(data, dict):
        return None

    # ۳) دستگاه باید در فهرستِ سفید باشد. هر چیزِ دیگری رد.
    spec = ALLOWED.get(data.get("device"))
    if spec is None:
        return None

    # ۴) مقدار باید عدد باشد؛ رشتهٔ "90" عدد نیست.
    #    نکته: در پایتون True یک عددِ صحیح (برابرِ 1) به‌حساب می‌آید.
    value = data.get(spec["key"])
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        return None

    # ۵) عدد را به بازهٔ فیزیکیِ قطعه بچسبان. این خط قابلِ حذف نیست.
    value = max(spec["min"], min(spec["max"], int(value)))

    return {"device": data["device"], "value": value}


def apply_command(cmd):        # فقط فرمانی که از صافی رد شده به اینجا می‌رسد
    if cmd["device"] == "servo":
        servo.write(cmd["value"])
    elif cmd["device"] == "led":
        led.duty(cmd["value"])
    elif cmd["device"] == "buzzer":
        buzzer.beep(cmd["value"])


def stop_all():                # حالتِ امن: هرچه در حرکت است می‌ایستد
    led.duty(0)
    buzzer.beep(0)


reply = ask_model(user_sentence)   # متنِ خامی که از API برگشته
cmd = parse_command(reply)

if cmd is None:
    stop_all()                     # پیش‌فرض: هیچ کاری نکن
    print("invalid command, ignored")
else:
    apply_command(cmd)

parse_command هیچ سخت‌افزاری را دست نمی‌زند و فقط قضاوت می‌کند؛ apply_command هیچ قضاوتی نمی‌کند و فقط چیزی به دستش می‌رسد که از صافی رد شده. بینِ خروجیِ مدل و پایهٔ GPIO دقیقاً یک راه هست، و آن راه از صافی می‌گذرد.

زمان و پول

تأخیر: چرا مدل هرگز داخلِ حلقهٔ کنترل نمی‌رود

یک فراخوانیِ مدل روی WiFi باید آدرسِ سرور را پیدا کند، ارتباطِ رمزنگاری‌شده برقرار کند، درخواست بفرستد، منتظرِ تولیدِ جواب بماند و جواب را بگیرد؛ مجموعش در مرتبهٔ یک ثانیه یا بیشتر است و با هیچ ترفندی به میلی‌ثانیه نمی‌رسد. طرفِ دیگر، یک حلقهٔ کنترل — تعادل، PID، فرار از مانع — معمولاً ۱۰۰ تا ۱۰۰۰ بار در ثانیه اجرا می‌شود:

حلقهٔ کنترل با نرخ 100 بار در ثانیه ->  هر تکرار 10 میلی‌ثانیه
یک فراخوانیِ مدل                    ->  حدودِ 1.5 ثانیه  =  ~150 تکرارِ از دست رفته
رباتی با سرعتِ 30 cm/s              ->  در همان 1.5 ثانیه ~45 سانتی‌متر جلو می‌رود

پس این ممنوع است: فراخوانیِ مدل داخلِ حلقهٔ تعادل، داخلِ PID، داخلِ منطقِ فرار از مانع، یا هرجایی که یک وقفهٔ یک‌ثانیه‌ای یعنی برخورد — حتی «فقط یک بار برای اطمینان»، چون همان یک بار در بدترین لحظه اتفاق می‌افتد. این مجاز است: گزارشِ دوره‌ای، واکنش به یک رویداد، پاسخ به جملهٔ کاربر، خلاصه‌سازیِ روزانه — مشترکشان این است که چند ثانیه صبر کردن در آن‌ها هیچ معنایی جز صبر کردن ندارد. و راهِ عملی: فراخوانی را از حلقه بیرون بگذار تا حلقهٔ کنترل با تمامِ سرعت کارش را بکند و جواب، هر وقت رسید، فقط یک متغیر را عوض کند.

هزینه: چطور «هر ثانیه» به یک صورت‌حساب تبدیل می‌شود

هر فراخوانی پول دارد و پول با token حساب می‌شود، هم چیزی که فرستادی هم چیزی که برگشت. نرخ‌ها و مدل‌ها مدام عوض می‌شوند، پس نرخ را از صفحهٔ خودِ سرویس بخوان؛ چیزی که عوض نمی‌شود این حساب است:

تعدادِ فراخوانی در ماه × (توکنِ ورودی + توکنِ خروجی) × نرخ = هزینهٔ ماهانه

هر ۱ ثانیه یک بار  ->  86,400 در روز  ->  ~2,592,000 در ماه
هر ۱ دقیقه یک بار  ->   1,440 در روز  ->     ~43,200 در ماه
هر ۵ دقیقه یک بار  ->     288 در روز  ->      ~8,640 در ماه
هر ۱ ساعت یک بار   ->      24 در روز  ->        ~720 در ماه
روزی یک بار         ->       1 در روز  ->         ~30 در ماه

بینِ سطرِ اول و آخر حدودِ ۸۶ هزار برابر فاصله است، و در بیشترِ پروژه‌ها هر دو سطر «همان کار» را انجام می‌دهند: خواندن مالِ برد است، فرستادن مالِ لحظه‌ای که حرفی برای گفتن هست. سه کار این ضرب‌کننده را کوچک می‌کند — رویدادمحور بفرست (فقط وقتی عدد از حدی گذشت؛ یک گلدانِ بی‌مشکل شاید ماهی سه بار حرفی برای گفتن داشته باشد)، روی برد خلاصه کن (به‌جای ۸۶ هزار نمونه، بیشینه و کمینه و میانگین و روند)، و جوابِ کوتاه بخواه، که مستقیم روی توکنِ خروجی اثر می‌گذارد.

و نکته‌ای که مخصوصِ ماست: متنِ فارسی توکنِ بیشتری می‌گیرد — همان معنا، توکنِ بیشتر — یعنی هر تخمینی که از منبعی انگلیسی برداشته‌ای برای پروژهٔ فارسیِ تو کم‌برآورد است (توکن چیست). برای مصرفِ واقعی به بخشِ usage در جوابِ API نگاه کن.

کلید کجا زندگی می‌کند

کلیدِ API مثلِ رمزِ عبور است: هرکس داشته باشد به حسابِ تو فراخوانی می‌زند و پولش را تو می‌دهی. سه جا هست که نباید باشد: داخلِ فایلی که در گیت ثبت می‌شود — پاک‌کردنش در کامیتِ بعدی فایده ندارد چون تاریخچه نگهش می‌دارد، و خزنده‌ها مخزن‌های عمومی را دقیقاً برای همین می‌گردند؛ داخلِ اسکچی که برای کسی می‌فرستی؛ و در تصویری که از صفحهٔ کدت می‌گیری، که از همه بیشتر اتفاق می‌افتد. کارِ درست: اول، کلید را در فایلی جدا بگذار و آن فایل را از گیت بیرون نگه دار.

# secrets.py — نامش در .gitignore هست و هرگز ثبت نمی‌شود
WIFI_SSID = "..."
WIFI_PASS = "..."
API_KEY   = "..."

# main.py — فقط وارد می‌کند، خودش هیچ کلیدی ندارد
from secrets import WIFI_SSID, WIFI_PASS, API_KEY

کنارش یک secrets.example.py با مقدارهای خالی بگذار تا هرکس پروژه را برداشت بداند چه لازم دارد. دوم، اگر می‌شود کلید اصلاً روی برد نباشد: برد به واسطه‌ای کوچک از خودت وصل شود — برنامه‌ای ساده روی لپ‌تاپ یا یک سرورِ کوچک — و آن واسطه با مدل حرف بزند. سه چیز را با هم حل می‌کند: کلید در دستگاهی که ممکن است گم شود نیست، می‌توانی سقفِ مصرف بگذاری، و ارتباطِ رمزنگاری‌شده روی سخت‌افزارِ قوی‌تری انجام می‌شود — مستنداتِ خودِ سازنده برای دست‌دادنِ TLS روی ESP32 حدودِ ۴۰ تا ۵۰ کیلوبایتِ حافظهٔ آزاد لازم می‌داند، از همان ۵۲۰ کیلوبایتی که بخشِ بزرگش را WiFi و firmware برداشته‌اند. سوم، کلید را دوره‌ای عوض کن و اگر شک کردی لو رفته همان لحظه باطلش کن.

یک پروژه که همین هفته تمام می‌شود

گلدانی که وضعش را به فارسی گزارش می‌دهد — یک ESP32، حسگرِ رطوبتِ خاک با خروجیِ آنالوگ، حسگرِ دما، یک نمایشگرِ OLED یا حتی فقط خروجیِ سریال، و یک LED.

  • برد چه می‌خواند: هر ۱۵ دقیقه رطوبت و دما. عددِ خام از ADC می‌آید و با دو اندازه‌گیری به درصد کالیبره می‌شود — خاکِ کاملاً خشک، و خاکِ تازه‌آب‌خورده. پایهٔ آنالوگ را از روی نقشهٔ پایه‌های GPIO در ESP32 انتخاب کن؛ همهٔ پایه‌ها آنالوگ نیستند و بعضی‌شان وقتی WiFi روشن است به کار نمی‌آیند.
  • چه فرستاده می‌شود: شبی یک بار یا با فشارِ دکمه — نه ۹۶ نمونهٔ خام، بلکه خلاصه: رطوبتِ فعلی، کمینه و بیشینهٔ امروز، تفاوت با دیروز، دما، و تعدادِ روزها از آخرین آبیاری. چه برمی‌گردد: یک جملهٔ فارسی که فقط نمایش داده می‌شود.
  • چه حرکت می‌کند: یک LED قرمز که با if humidity < DRY_THRESHOLD در کدِ خودت روشن می‌شود، نه با نظرِ مدل. حتی اگر جملهٔ برگشته بگوید «گلدان حالش خوب است»، LED بر اساسِ عددِ واقعی روشن می‌ماند — دقیقاً الگوی سوم: مدل حرف می‌زند، عدد تصمیم می‌گیرد.

چطور امن شکست می‌خورد — این را از اول طراحی کن. WiFi وصل نشد؟ نمایشگر عددها را نشان می‌دهد و کنارش «بدونِ شبکه». فراخوانی خطا داد یا timeout شد؟ همان رفتار، و تلاشِ بعدی در نوبتِ بعدی، نه حلقهٔ تلاشِ بی‌پایان. جوابِ عجیب یا خالی برگشت؟ چاپ نمی‌شود و آخرین جملهٔ معتبر می‌ماند. حسگر عددِ بیرون از بازه داد؟ آن نمونه دور ریخته می‌شود. یعنی بدترین حالتِ این پروژه تنزل به یک نمایشگرِ عددیِ ساده است — همان چیزی که پیش از وصل‌کردنِ مدل داشتی.

معیارِ خوبی است برای هر پروژه‌ای که مدل به آن وصل می‌شود: اگر مدل را از مدار بیرون بکشی، چیزی که می‌ماند باید هنوز کار کند. قدمِ بعدی یک پمپِ کوچک است با تغذیهٔ جدا و groundِ مشترک — ولی تصمیمِ آبیاری همچنان مالِ همان if است. جدا نگه‌داشتنِ «تصمیم» از «کنترل» همان چیزی است که در دورهٔ رباتیک آیرو روی پروژه‌های بزرگ‌تر تمرین می‌شود.

شش کاری که می‌توانی همین امروز شروع کنی

۱. اول پروژه را بدونِ مدل تمام کن. حسگر بخوان، خروجی بده، مطمئن شو پایدار است. وگرنه وقتی چیزی خراب شد نمی‌فهمی تقصیرِ سیم است، کد است، شبکه است یا مدل.

۲. اولین فراخوانی را از روی لپ‌تاپ بزن، نه از روی برد. قالبِ JSON و پرامپت و کلید را آنجا درست کن؛ انتقالش به برد بعد از آن کارِ مکانیکی است. اگر از اول روی برد شروع کنی، همزمان با چند مسئلهٔ ناهم‌جنس می‌جنگی.

۳. تأخیر و مصرفِ خودت را اندازه بگیر. زمان را پیش و پس از فراخوانی ثبت کن و ده بار تکرارش کن، و مصرفِ token را از بخشِ usage بردار. مرتبه‌هایی که اینجا آمد نقطهٔ شروع‌اند، جوابِ تو نیستند.

۴. صافیِ ورودی را پیش از اولین حرکت بنویس. پیش از وصل‌کردن به سخت‌افزار خودت متن‌های بد به parse_command بده و ببین همه را رد می‌کند: یک JSON ناقص، یک متنِ کاملاً معمولی، عددِ ۹۰۰، عددِ منفی، رشتهٔ "90"، دستگاهی به نامِ rocket.

۵. پیش از هر فرمانِ سخت‌افزاری یک clamp بگذار — حتی وقتی مطمئنی لازم نیست. این خط قرار است روزی جانِ قطعه‌ات را بخرد که حواست نبوده، و هزینه‌اش یک خط است.

۶. کلید را از همان اولین فایل جدا کن. secrets.py بساز، نامش را در .gitignore بگذار، و کنارش یک secrets.example.py خالی؛ آن‌وقت هیچ‌وقت لازم نمی‌شود تاریخچهٔ گیت را پاک‌سازی کنی.

و در آخر

جوابِ آن سؤالِ اول این است: نه، مدل روی برد اجرا نمی‌شود و فاصله‌اش صدها برابر است، نه کمی. ولی سؤال از اول کمی کج بود — کاری که واقعاً می‌خواستی، اینکه پروژه‌ات زبان بفهمد و زبان تولید کند، کاملاً شدنی است، فقط با تقسیمِ کارِ دیگری: برد دست و حس است، مدل زبان است، و بینِ این دو لایه‌ای از بررسی می‌نشیند که تو نوشته‌ای.

آن لایه، حرفِ اصلیِ این نوشته است. مدلِ زبانی متن می‌سازد و متن گاهی غلط است؛ این تغییر نمی‌کند. چیزی که در دستِ توست این است که غلط بودنِ متن چه هزینه‌ای داشته باشد: اگر خروجی مستقیم به موتور برسد هزینه‌اش سخت‌افزار است، و اگر از یک فهرستِ سفید و یک clamp و یک پیش‌فرضِ «هیچ کاری نکن» رد شود، هزینه‌اش یک خط در log است.

توهم در هوش مصنوعی می‌گوید چرا این متن‌ها با اطمینانِ کامل غلط می‌شوند، پرامپت‌نویسی می‌گوید چطور از همان اول جوابِ ساختاریافته‌تری بگیری، و توکن چیست می‌گوید صورت‌حسابِ فارسیِ این ماجرا از کجا می‌آید.