من هر روز به مدت یک ماه از عامل تغذیه هوش مصنوعی خود استفاده کردم. در اینجا چیزی است که من در واقع باید اصلاح می کردم

خلاصه ۲۰۰ کلمهای به زبان فارسی:
با راهاندازی اولیه NutriAgent، مشکلات واقعی شروع شد. دادههای تغذیهای بین رابط تلگرام و وب جدا میشدند و تحلیل روزانه را غیرممکن میکردند. برای حل این تکهتکه شدن، سیستم ادغام حسابها طراحی شد: از تنظیمات وب یک کد کوتاهمدت دریافت، به تلگرام ارسال و سیستم شناسه کاربران را (با استفاده از clerk_id) یکپارچه کرد. این کار بهجای استفاده چندکاربره، فقط یک هویت واحد برای تمام پلتفرمها ایجاد کرد.
مشکل دوم، 처리 چند تصویر در تلگرام بود. هر عکس بهطور جداگانه تحلیل میشد و 결과 متناقض میداد. با پیادهسازی یک سیستم Batch Processing در هندلر تلگرام (با استفاده از MediaGroupHandler، تاخیر ۱ ثانیهای و بوفرسازی تصاویر)، ارسال گروهی عکسها فراهم شد. این لایهبندی منطق Telegram از کد اصلی Agent جدا نگه داشت و سازگاری پلتفرم را تضمین کرد.
پس از اصلاحات، کاربری پایدتر و یکپارچه رخ داد. این تجربه یادآور این بود که ساختن محصولات واقعی، کشف دردهای UX است که در نمونهسازی اولیه آشکار نمیشوند. موفقیت نه فقط در اجرای ایده، که در حل مشکلات کاربردی در دنیای واقعی است. NutriAgent اکنون به یک محصول قابل استفاده ارتقا یافته است.
یک ماه پیش، درباره ساخت NutriAgent نوشتم، ردیاب تغذیه هوش مصنوعی من که وعدههای غذایی را از تلگرام و وب در برگه Google من ثبت میکند (میتوانید پست اصلی را اینجا بخوانید). من آن را به کار انداختم، مقاله را پست کردم و فهمیدم که این پایان داستان است.
سپس شروع کردم به استفاده از آن هر روز. و این زمانی بود که مشکلات واقعی شروع به نمایان شدن کردند.
نه باگ. تصادف نمی کند. فقط… چیزهای کوچکی که باعث شد چندین بار در روز فکر کنم “صبر کن، این آزار دهنده است”. چیزهایی که فقط زمانی متوجه می شوید که کاربر واقعی یک مشکل واقعی را حل می کند، نه فقط یک ایده جالب را نمایش می دهد.
دو مشکل تجربه را کاملاً شکست.
مشکل دو صفحه گسترده (چرا داده های من شکسته شده اند)
صبحانه ام را سریع از گوشیم در تلگرام ثبت می کردم. سپس هنگام ناهار، پشت کامپیوترم بودم و از رابط وب استفاده میکردم، زیرا آسانتر بود. اما در پایان روز، وقتی میخواستم تجزیه کامل تغذیهام را ببینم، دادههایم را در دو حساب مختلف و دو صفحه گسترده مختلف تقسیم کردم. من مجبور شدم به صورت دستی ردیف ها را کپی کنم و آنها را ادغام کنم تا یک کل روزانه ساده به دست بیاورم.
این نماینده وعده های غذایی تلگرام من را تحت یک شناسه کاربری ذخیره کرده است. چت های وب من تحت دیگری بود. وقتی پرسیدم “این هفته چی خوردم؟” پاسخ کاملاً به پلتفرم مورد استفاده من بستگی داشت. دادههای تغذیهای من تکهتکه بود، و هرگونه تحلیل واقعی را غیرممکن میکرد.
متوجه شدم که “آن را چند کاربره کنید” کافی نیست. من به یک هویت در هر دو کانال نیاز داشتم.
از آنجایی که هر دو کانال را برای سناریوهای مختلف مفید یافتم، تصمیم گرفتم راهی برای استفاده از آنها بیابم در حالی که دادههایم را یکپارچه نگه میدارم و برای تجسم و تجزیه و تحلیل آسان نگه دارم.
چگونه پیوند در واقع کار می کند
من به این فکر کردم که این ویژگی را در عامل اصلی به عنوان ابزاری برای این کار ایجاد کنم: “ایمیل خود را برای پیوند دادن حساب خود ارسال کنید.” اما تایپ ایمیلها در چت سخت بود. انتظار برای کدهای تایید در تلگرام کندتر از کلیک کردن روی یک دکمه بود.
برخی از ویژگی ها فقط در یک رابط وب سریعتر هستند. پیوند دادن حساب یکی از آنهاست.
بنابراین من یک صفحه تنظیمات در برنامه وب ایجاد کردم که یک کد پیوند کوتاه مدت تولید می کند. شما آن را کپی می کنید، آن را در تلگرام قرار می دهید و ربات حساب های شما را متصل می کند. همین است.
جریان:
- یک کد از تنظیمات وب دریافت کنید
- برای ربات تلگرام ارسال کنید
- Backend شما را اعتبارسنجی و متصل می کند
telegram_user_idبه شماclerk_user_id - تاریخچه چت و گزارش تغذیه را ادغام کنید تا همه چیز را در یک حساب کاربری نگه دارید

زیر سرپوش: یک کاربر، دو کانال، یک منبع حقیقت
در زیر کاپوت، تصمیم اصلی این بود که یک هویت کاربر متعارف را انتخاب کنید و هر چیز دیگری را مجبور کنید تا با آن هماهنگ شود.
در سمت وب، احراز هویت توسط Clerk انجام می شود که به من یک استیبل می دهد clerk_user_id. به جای اختراع یک سیستم هویت موازی برای تلگرام، تصمیم گرفتم بسازم clerk_user_id کلید اصلی در همه جا
در باطن، مدل کاربر اکنون تقریباً شبیه به این است:
-
clerk_user_id→ شناسه اولیه -
telegram_user_id→ اختیاری، باطل -
email→ ابرداده و اشکال زدایی
این یعنی:
- تلگرام دیگر یک «کاربر مجزا» نیست
- این فقط یک رابط دیگر است که به همان حساب متصل شده است
- همه گزارشهای تغذیه، تاریخچه چت و خلاصهها با یک شناسه کلید زده میشوند
جریان کد پیوند عمدا ساده است:
- برنامه وب یک کد کوتاه مدت ایجاد می کند
clerk_user_id - تلگرام کد را به باطن ارسال می کند
- اگر معتبر باشد، Backend پیوست می شود
telegram_user_idبه رکورد کاربر موجود
بدون حدس زدن بدون اکتشاف. بدون تطبیق ایمیل
اگر کد مطابقت داشته باشد، کاربر به صراحت قصد دارد حساب ها را پیوند دهد.
این محدودیت کوچک کل دسته ای از موارد لبه را که نمی خواستم بعداً اشکال زدایی کنم حذف کرد.
سردرد تلگرامی “یک وعده غذایی، سه پیام”.
هنگامی که هر دو کانال به آرامی کار می کنند، شروع به استفاده از آنها به جای یکدیگر کردم. آن موقع بود که متوجه چیز دیگری شدم. نسخه وب به من امکان می دهد چندین تصویر را به یک پیام متصل کنم، به عنوان مثال، یک عکس از غذای من به اضافه یک اسکرین شات از برچسب تغذیه. این امر تخمین های هوش مصنوعی را بسیار دقیق تر کرد.
اما وقتی همین کار را در تلگرام امتحان کردم، سه پیام جداگانه منتشر کرد و سه پاسخ هوش مصنوعی جداگانه با تعداد کالری متفاوت دریافت کردم. هر عکس به صورت مجزا از وب هوک، بدون زمینه عکس های دیگر پردازش شد. شکاف تجربه ناامید کننده بود. نماینده در وب احساس هوشمندی کرد، در تلگرام شکست.
چگونه مشکل چندین تصویر را رفع کردم
تلگرام راهی برای شناسایی گروههای رسانهای که ارسال میشوند، دارد MediaGroupHandler در کنترل کننده وب هوک برای زمانی که چندین عکس را همزمان ارسال می کنید. بنابراین من یک سیستم دسته بندی ساده ساختم:
- هنگامی که ربات تصویری را به عنوان بخشی از یک گروه رسانه ای دریافت می کند، 1 ثانیه صبر می کند تا پردازش درخواست را آغاز کند
- اگر تصاویر بیشتری در آن چت در پنجره وارد شود، آنها را گروه بندی می کند و تاخیر را بازنشانی می کند
- همه آنها را به عنوان ارسال می کند
list[bytes]به نماینده در یک تماس
مال عامل analyze() روش قبلاً پذیرفته شده است list[bytes]، بنابراین هیچ تغییری در آنجا لازم نیست. رفع مشکل صرفاً در هندلر تلگرام بود.
اکنون می توانم سه زاویه از بشقاب خود را به اضافه یک برچسب تغذیه بفرستم و یک پاسخ هوشمند دریافت کنم.
چرا این اصلاح در لایه تلگرام (نه Agent) وجود دارد
یک جزئیات مهم: من اصلا نماینده رو عوض نکردم برای پشتیبانی از چندین تصویر
نماینده قبلا قبول کرده است list[bytes] برای تصاویر اشکال واقعی قابلیت مدل نبود – این بود ارکستراسیون پیام.
تلگرام تصاویر را به صورت زیر ارسال می کند:
- رویدادهای وب هوک جداگانه
- گاهی اوقات با a گروه بندی می شود
media_group_id - گاهی اوقات با فاصله میلی ثانیه از هم می رسند، بدون نظم
در اصل، هر وب هوک بلافاصله یک تماس نماینده را راه اندازی می کرد. یعنی:
- یک تصویر = یک تحلیل
- زمینه مشترک صفر
- تخمین های کالری متناقض
راه حل این بود که پیام های تلگرام را به عنوان تلقی کنیم سیگنال ها، نه درخواست ها
من یک لایه بچینگ سبک در هندلر تلگرام معرفی کردم:
- تصاویر با همان
media_group_idبافر شده اند - یک پنجره کوتاه (1 ثانیه) منتظر تصاویر بیشتر است
- هر تصویر جدید تایمر را بازنشانی می کند
- وقتی پنجره بسته می شود، همه تصاویر با هم ارسال می شوند
از نظر مفهومی این است:
“صبر کنید تا صحبت کاربر تمام شود، سپس فکر کنید.”
media_groups: dict[str, list[bytes]] = {}
tasks: dict[str, asyncio.Task] = {}
lock = asyncio.Lock()
async def handle_image(chat_id, media_group_id, image_bytes):
async with lock:
media_groups.setdefault(media_group_id, []).append(image_bytes)
if media_group_id in tasks:
tasks[media_group_id].cancel()
tasks[media_group_id] = asyncio.create_task(
process_after_delay(media_group_id, chat_id)
)
async def process_after_delay(media_group_id, chat_id):
await asyncio.sleep(1)
images = media_groups.pop(media_group_id, [])
await agent.analyze(images=images, chat_id=chat_id)
با نگه داشتن این منطق در داخل آداپتور تلگرام:
- عامل سکوی-آگنوستیک باقی می ماند
- همین خط لوله تجزیه و تحلیل برای بارگذاری های وب، آلبوم های تلگرام یا مشتریان آینده تلفن همراه کار می کند
- ابهامات تلگرام به منطق اصلی کسب و کار نفوذ نمی کند
این یکی از آن اصلاحاتی بود که همه چیز را ساخت احساس کنید هوشمندتر بدون پیچیده تر کردن سیستم
یکی دیگر از عوارض جانبی این پیاده سازی این بود که من را مجبور کرد به برنامه نویسی ناهمزمان با FastAPI و Uvicorn عمیق تر بروم. من قبلاً در معرض asyncio قرار گرفته بودم، اما این اولین باری بود که باید به صراحت در مورد زمانبندی، لغو، و وضعیت اشتراکگذاری در جریان واقعی کاربر استدلال میکردم.
برای ساده نگه داشتن راه حل، من از ذخیره سازی در حافظه ترکیب شده با asyncio.Lock() و asyncio.Tasks قابل لغو برای پیاده سازی منطق دسته بندی و debounce استفاده کردم. این به خوبی کار می کند زیرا ربات در حال حاضر با یک کارگر اجرا می شود، بنابراین من نیازی به هماهنگی یا اصرار خارجی ندارم.
بخش مهم این است که این یک میانبر نبود – یک معاوضه آگاهانه بود. اگر من نیاز به مقیاس افقی داشته باشم، همین الگو به وضوح به Redis، یک صف یا یک کارگر پس زمینه ترجمه می شود. در حال حاضر، راهحل سادهتر، سیستم را آسانتر برای استدلال، آزمایش و تکامل نگه میدارد.
لحظه “اوه، این واقعاً آرام است” لحظه
پس از تغییرات، در یک استراحت ناهار را در تلگرام وارد کردم، زمانی که پشت کامپیوتر بودم از چت اینترنتی استفاده کردم و آن شب، صفحه گسترده را با تصویر کامل روزم برای تجزیه و تحلیل و مقایسه با بقیه هفته باز کردم.
من سه عکس از شام فرستادم – بدون هرزنامه، فقط یک پاسخ تمیز. در نهایت محصول به جای چسباندن چسب به هم، عمدی احساس می شود.
آنچه آزمایش آزمایشی در واقع به شما می آموزد
ساختن برای خودتان با ساختن برای یک کاربر فرضی متفاوت است. شما بلافاصله درد را احساس می کنید. شما نمی توانید UX بد را نادیده بگیرید زیرا این شما هستید که در رنج هستید.
شکاف بین «این کار می کند» و «به اندازه کافی برای استفاده روزانه کار می کند» بسیار زیاد است – و فقط آزمایش های آزمایشی آن را آشکار می کند.
من یاد گرفتم که مهندسی زمینه مهمتر از بارگذاری بیش از حد دستورات است. من یاد گرفتم که برخی از ویژگیها متعلق به رابطهای کاربری وب هستند، نه چت. و یاد گرفتم که شروع با یک ابزار بدون کد برای آزمایش عالی است، اما استفاده واقعی نیاز به معماری واقعی دارد.
اکنون یک محصول واقعی است
NutriAgent زمانی که به آن نیاز پیدا کردم دیگر یک پروژه اسباب بازی نبود. این تغییرات فقط ویژگیهایی را اضافه نکردند، بلکه آن را به چیزی تبدیل کردند که بتوانم آن را به اشتراک بگذارم و مقیاسبندی کنم.
این پروژه به صورت زنده در https://nutriagent.juandago.dev است. کد برای Agent و Web UI منبع باز است.
این سفر من بود، اما دوست دارم نظرات شما را بشنوم. بیایید گفتگو را در X یا LinkedIn ادامه دهیم.



