از دستیار کدنویس تا همکار هوشمند؛ گام اول: کابوس مستندسازی

بیایید واقع‌بین باشیم: نگه‌داری مستندات واقعاً اعصاب‌خُرده‌کن ـه!

احتمالا همه‌امون با پروژه‌ای رو به رو شدیم که تو README فقط این نوشته بود: "TODO: Add documentation". یا حتی بدتر... داک‌هایی که آنقدر قدیمی‌ان که فقط باعث گمراهی می‌شن. بعدشم اینطوری بودیم که: کلی وقت برای فهمیدن پروژه گذاشتیم، به خودمون قول دادیم که این‌بار داکش می‌کنیم. ولی نـــــه... deadline‌ها و باگ‌های پروداکشن می‌رسن و مستند‌سازی می‌ره ته لیست کارها.

چرخه‌ی مرگ مستند‌ها

ماجرا معمولاً این‌طوری شروع می‌شه:

  • شروع قوی: این بار دیگه داک رو همیشه آپدیت نگه می‌داریم!

  • واقعیت می‌خوره تو صورتمون: آخرهای اسپرینته، باگ‌های پروداکشن، فیچر جدید و داکی که عقب میافته.

  • جمع شدن بدهی فنی: کد تغییر می‌کنه، dependencyها آپدیت می‌شن، تصمیمات معماری هم عوض می‌شن.

  • داک‌ بو می‌گیرن: همون داک نصفه‌نیمه هم، یه روز می‌بینیم کاملاً غلط و قدیمی شده و دیگه به‌روز نیست.

  • تازه‌واردها عذاب می‌کشن: onboarding تبدیل می‌شه به حفاری معدن؛ باید کدها رو خط‌به‌خط خوند تا فهمید سرویس چیکار می‌کنه.

و این چرخه‌ی مریض ادامه داره: همه مدام دنبال حل مشکل فوری‌ان و کسی حوصله رسیدن به ریشه ماجرا رو نداره.

ورود به عصر هوش‌مصنوعی

وقتی ChatGPT و Claude و بقیه مدل‌های هوش‌مصنوعی وارد میدون شدن و در فهم کد پیشرفت کردن، به این فکر افتادیم: واقعاً نمی‌شه این کارهای عذاب‌آور رو خودکار کرد؟ منظور صرفا تولید یه قالب و متن کلی از پروژه نیست؛ واقعا مثل یک برنامه‌نویس باتجربه کد رو تحلیل و مستند کنن. نکته اینجاست که مدل‌های هوش‌مصنوعی در تشخیص الگوها واقعاً خوبن؛ فقط باید درخواست و context رو براشون درست آماده کنید. اما LLMها و هوش مصنوعی امروزی، یک چالش خیلی بزرگ دارن، که اونم محدودیت «حافظه» یا همون «پنجره کانتکست» (Context Window) اون‌هاس. خیلی ساده یعنی نمی‌تونیم کل دانش و داده یک پروژه رو، مثلاً تمام کدها (Codebase)، یک‌باره جلوی مدل بذاریم و انتظار یک تحلیل عمیق و درست و حسابی داشته باشیم.

اما خب شاید بگید مدل‌های مثل Gemini که یک میلیون توکن ورودی می‌گیرن چطور؟ مشکل اینه وقتی مدل با حجم عظیمی از داده روبرو می‌شه، عملاً در اطلاعات غرق می‌شه. این اتفاق باعث می‌شه که از یک طرف، فرآیند تحلیل به شدت کند و گران تموم بشه و از طرف دیگه، کیفیت جواب‌ها افت کنه. مدل یا مسئله رو خیلی کلی و سطحی حل می‌کنه که به دردمون نمی‌خورد، یا کاملاً گیج می‌شه و شروع می‌کنه به «هذیان‌گویی» (Hallucination) و بافتن کلمات بی‌ربط به هم.

راه حل ما دقیقاً برای رفع همین مشکل بزرگ بودن ابعاد پروژه‌ها طراحی شد. به جای اینکه هر بار مدل رو در این فرآیند ناقص غرق کنیم، با یک بار تحلیل جامع و عمیق، اکثریت کانتکست مورد نیازش رو استخراج کنیم و در یک «حافظه پایدار» براش نگهداری کنیم. به این ترتیب، هوش مصنوعی همیشه پیش‌زمینه لازم برای ارائه پاسخ‌های دقیق، سریع و به مراتب کم‌هزینه‌تر رو در اختیار داره.

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

پس رفتیم سراغ اینکه سعی کنیم مسئله رو بشکونیم، و هر مسئله روی بخشی از سوال متمرکز باشه. اینطوری Agentهای مختلفی می‌سازیم که روی بخشی از حل مسئله متمرکزن.

رویکرد Multi-Agent

برای حل مسئله، اول با این شروع کردیم که اگر جای LLM واقعا یک انسان داشتیم چطور این مسئله‌ رو حل می‌کرد؟ براساس همین طرز فکر به این پنج تا ایجنت رسیدیم:

۱.Structure Agent: اولین قدم برای هر توسعه‌دهنده شناخت معماری پروژه‌اس. از ساختار ماژول‌ها و اینترفیس‌ها بگیر تا متودهای مهم و پترن‌های پروژه. این Agent، الگوهای معماری رو پیدا می‌کنه، روابط اجزای مختلف رو شرح می‌ده و ساختار کلی پروژه رو مستندسازی می‌کنه — همون چیزی که معمولاً روزها طول می‌کشه تا مستند کاملی ازش درست کنیم.

۲. Data Flow Agent: مرحله بعدش، دنبال کردن جریان داده تو پروژه‌‌اس. معمولا توی هر پروژه، کلی داده داریم که از یه جایی تولید می‌شن و توی سیستم می‌چرخن. یه تغییری می‌کنن و تهش سر از یجای دیگه درمیارن. این Agent رد پای دیتا رو از مدل‌ها، Transformationها، دیتابیس‌ها و غیره می‌گیره و چرخه تغییر داده رو، سعی می‌کنه توضیح بده.

۳. Request Flow Agent: حالا باید بفهمیم وقتی درخواستی به سیستم می‌رسه، مثلا وقتی یک اندپوینت HTTP یا RPC صدا زده میشه، چطوری به سیستم می‌رسه، چه اعتبارسنجی‌هایی روش انجام می‌شه و چطوری توی سیستم مسیریابی میشه تا به جای خاصی برسه و … . این Agent کل مسیر درخواست رو از سر تا ته قضیه مستند می‌کنه.

۴. Dependency Agent: تهشم، باید Dependencyها رو بسنجیم. اینکه سیستم ما چه وابستگی‌های داخلی و خارجی‌ای داره، به چه سرویسی ریکوئست میزنه، چه پکیج‌هایی استفاده کرده و … .این Agent نه‌تنها وابستگی‌ها رو لیست می‌کنه، بلکه مشکلاتشون، Circular Dependency، و ... رو هم می‌سنجه — تفاوت بین یه pip freeze ساده با شناخت بهتر از وابستگی‌هات.

۵. API Agent: این Agent از دل دستیارهای داخلی‌امون درومد. موقعی که دیدم خیلی از توسعه‌دهنده‌ها، سوال‌هایی مثل "چطور این endpoint رو دستی کال کنم؟"یا "ریسپانس خروجی این endpoint چیه؟" رو می‌پرسند. کارش هم ساده‌اس. پیدا کن چه APIهایی رو سرویس serve می‌کنه یا کجاها رو کال میکنه. اینا رو مستند کن.

نکته طلایی: LLMها و AIرو مثل برنامه‌نویس‌ها و آدم‌ها نگاه کن. مشکل رو همونطوری حل کن که توسعه‌دهنده‌ی حرفه‌ای حل می‌کنه. هر Agent یک دیدگاهی داره، و ترکیب‌شون همون روالی رو بازسازی می‌کنه که ما به‌شکل انسانی انجام می‌دیم

خروجی واقعی: context بهتر برای همه

تو خونه‌ی اول این داک‌ها رو برای دستیارهای هوشمند داخلی خودمون آماده‌سازی کردیم. این دستیارها برای فهم پروژه دیگه کافی بود Readme و AI Docها رو بخونن و درجا سراغ تحلیل مستقیم کد نرن. ساختار داک رو جوری طراحی کردیم که هم خواندنش برای آدم راحت باشه، هم برای AI به شکل ساختارمند و مناسب استفاده بشه.

مثلاً وقتی دستیار کدمون باید جواب بده "کدوم سرویس مسئول احراز هویت کاربره؟"، اول سراغ مستندهای apiامون می‌ره، بعد اگر نشد سراغ کدها و ... . عملا این داک‌ها نقش مکانیزم کشف (discovery) برای AI رو همزمان بازی می‌کنه.

ولی اونجا جالب‌تر شد که ابزارهایی مثل Copilot، Claude و ChatGPT هم جواب‌های دقیق‌تر، مرتبط‌تر و کاربردی‌تر تولید کردن. چون context (پیش زمینه‌) رو از داک‌ها می‌خوندن و می‌تونستن براساس واقعیت پروژه، پیشنهاد بدن.

در اصل این بخشی از همون چیزیه که بهش میگن Context Engineering. یه سیستم که میتونه دیتای مورد نیاز AI Agentها رو در زمان مناسب بهشون برسونه.

واقعیت فنی

ساختن این سیستم فقط چند خط پرامپت نبوده، معماری باید هم ساده و هم مطمئن طراحی می‌شد.

تصمیم گرفتیم که فریم‌ورک‌های موجود رو بررسی کنیم و ببینیم چیا ترند هستن. منطقا دوس نداشتیم دوباره از اول خیلی چیزا رو اختراع کنیم و تا جای ممکن می‌خواستیم زمانمون رو روی پرامپت‌نویسی و سیم‌کشی بین Agentها بذاریم تا پیاده‌سازی‌های پایه‌ای.

طبق ستاره‌های گیت‌هاب، CrewAI جزو فریم‌ورک‌های برتر AI بود. بعد خوندن داک و مثال‌هایی که داشت، بنظر می‌اومد که می‌تونه کار ما رو خوب راه بندازه. اینکه از ابتدا با ذهنیت MultiAgent (Crew) ساخته شده، برای ایجنت‌ات یه سری کانسپت ساده مثل Role, Backstory و Goal تعریف میکنه تا راحت‌تر پرامپت رو بنویسی، و از همون اول مجموعه‌ای از ابزارهای از پیش آماده داره، باعث می‌شد که آپشن خوبی بنظر بیاد.

ماجرای CrewAI

اول همه‌چیز رو با CrewAI پیاده کردیم. در ظاهر می‌توانی role و crew agentهای مختلف بسازی، بقیه orchestration رو خودش انجام می‌ده. ولی در عمل این مشکلات رو داشت:

  • تفت‌دادن پرامپت‌ها: CrewAI پرامپت‌ها رو با قالب خودش قاطی می‌کرد، همه دقت و ظرافت پرامپت‌نویسی رو از بین می‌برد.

  • مشکلات Parse کردن ریسپانس: کلی ارور ‍‍‍‍Invalid response from LLM call - None or empty، مخصوصاً روی مدل‌هایی غیر از OpenAI.

  • کال‌های ناقص: taskها نصفه‌کاره می‌پریدن بیرون، بعدشم باید دستی ادامه می‌دادی.

  • کنترل ناقص: نمی‌تونستی کنترل کنی که یه تسک کی تموم بشه، یا بعضا انقدر ادامه پیدا می‌کرد تا به لیمیت برسه.

  • مشاهده‌پذیری پایین: به خودی خود، خیلی چیزی لاگ نمی‌کرد و مجبور بودی کلی Middleware براش توسعه بدی.

پس هم کنترل پرامپت رو از دست می‌دادی و هم Parse کردن ریسپانس غیرقابل پیش‌بینی بود. کلی پرامپت رو تنظیم می‌کردی و آخرش، خروجی خراب می‌شد! خلاصتا بعد از چندماه دست و پا زدن و نتیجه قابل اتکا نگرفتن، تصمیم گرفتیم که بازنویسی بکنیم سیستم رو. این دفعه، با توجه به تجربه‌ی یکی دیگه از تیم‌ها و توضیحات آدم‌ها توی ردیت و بلاگ‌پست‌ها، به Pydantic-AI رسیدیم.

چرا Pydantic-AI برنده شد؟

(چپ به راست شو) Pydantic-AI، برعکس CrewAI اصلاً کنترل داشتن روی رفتار و دیباگش رو پیچیده‌ نمی‌کنه؛ همچی رو شفاف و ساده و بدون افراط نگه‌ می‌داره. اینطور می‌شه فهمید هر مشکلی از کجاست، نه اینکه شاید Crew AI پرامپت‌هاش با خواسته‌ی ما تناقض داره. همچنین تعریف Agent جدید و پرامپتشون ساده‌ است و پرامپت‌ها دستکاری نمی‌شن. و درسته که ابزارهای از پیش آماده نداره، اما پیاده‌سازی ابزار توش ساده‌اس و tool callهای بهتری داره. و مهم‌تر اینکه کانتکست تسک‌هات باهم قاطی نمی‌شن و مستقل از هم کار می‌کنن.

همینطور ما خیلی بیشتر می‌تونستیم بهش اعتماد کنیم. parsing errorها حذف شد و اعتبارسنجی‌هایی که انجام می‌داد باعث می‌شد خروجی ریسپانس LLM همیشه مطابق اون ساختاری که ازش خواستیم باشه و اگر نبود، agent خودش اتومات دوباره امتحان می‌کرد و کل تحلیل دود نمی‌شد. همچنین روی ایجنت‌ها و کارهات تسلطی که داشتی هم خیلی بیشتر بود. لیمیت‌های مختلف، ریترای‌های دقیق‌تر به ازای هر چیزی، محدود کردن توکن‌های مورد استفاده و مشاهده‌پذیری بهتر.

Prompt Engineering به سبک مهندسی

یکی دیگه از کارهای مهم‌امون این بود که با پرامپت‌ مثل کد برخورد کردیم! به‌جای هاردکد کردن پرامپت‌ها وسط کد، با قالب‌سازی و توی یه فایل yaml نگهداریشون کردیم. (اینجا از Jijna2 استفاده کردیم). این باعث شد:

  • همه می‌تونستن مشارکت کنن: لازم نبود حتماً توسعه‌دهنده باشی تا پرامپت رو پیدا کنی و تغییر بدی. ساختارش ساده بود و تغییرش هم ساده.

  • ورژن کنترل: تمام تاریخچه تغییرات پرامپت‌ها قابل‌دیدن بودن. از طرفی بررسی یه نفر دیگه هم برای انتشار نهایی حتما لازم بود.

  • تغییرپذیری: قالب و متن هر پرامپت براساس ورودی و context منعطف بود و می‌تونست تغییر کنه.

  • نگهداری: پرامپت‌هامون اینور اونور پروژه ریشه نزده و یک‌جا میشه خوند و فهمید چی‌ان.

تجربه نشون داد تو پروژه‌های AI، اهمیت پرامپت معمولا از خود کد بیشتره! و باید با همون فرهنگ مهندسی مدیریت بشه.

مسئله ارزیابی

کلاً تولید هرچیزی با AI یک سوال اساسی داره: از کجا بفهمیم خروجی قابل‌قبوله؟

اولش مسئول هر پروژه، داک‌های تولیدی‌امون رو برای پروژش رو چک می‌کرد و بازخورد می‌داد. خوب بود، ولی scale نمی‌شد و با زیاد شدن تعداد پروژه‌ها، باید با آدم‌های بیشتری تعامل می‌کردیم و کار فرسایشی، طولانی و سخت می‌شد. باید روال ساده‌تری پیدا می‌کردیم.

برای اینکه ارزیابی راحت‌تری داشته باشیم، گفتیم با این داک‌ها، بیایم Readmeهای بامعنا و جامعی برای هر پروژه درست کنیم. این چندتا فایده داشت برامون، یکی اینکه خیلی از پروژه‌ها Readme درست و درمون نداشتن و لازم بود اپدیت بشن و هم اینکه خوندن یه Readme و ارزیابی‌اش، خیلی ساده‌تر از خوندن کلی داک بود. از طرفی، کلی پروژه هم داشتیم که Readmeهای خوبی داشتن (چه پروژه متن‌باز چه پروژه داخلی) و می‌تونستیم با همونا هم چک کنیم که چقدر خروجی‌هامون درسته. پس اینطوری همون اول کار، یه سری معیار آماده هم برای ارزیابی سیستمون داشتیم. از طرفی دیگه، می‌تونستیم Readmeهای دقیق‌تر و مفیدتری هم از چیزهای جنرال برای خودمون بنویسیم. از اینکه یه توضیح کلی از پروژه بده شروع می‌شد تا جایی که Request Flow و C4 Model و … رو توی Readme می‌ذاشت و رودمپ پروژه رو بهمون توضیح می‌داد.

پس عملا نوشتن README برامون یه متر خوب شد: اگر درست و شفاف کار پروژه و معماری و راه استفاده رو توضیح می‌داد، می‌فهمیدیم تحلیل درست بوده.

نتایج واقعی

توی چند ماه استفاده روی پروژه‌های داخل شرکت:

  • فرایند Onboarding سریع‌تر شد: توسعه‌دهنده‌های تازه‌ای که چند هفته‌ی اول رو درگیر آشنایی با پروژه‌های مختلف و ... بودن، خیلی سریع‌تر و توی یه روز، ساختار و معماری پروژه‌ها رو فهمیدن.

  • زیرخاکی‌ها پیدا شدن: مثلاً یک سرویس هنوز از API منسوخ استفاده می‌کرد که همه یادشون رفته بود. از طرفی هم یه سری پترن‌های پروژه‌ها هم بالاخره مکتوب شد.

  • اتصال GitLab: یه کرون‌جاب که هرهفته اجرا می‌شه، هر ریپویی که تغییر جدید داره، آنالیز میشه و داکش آپدیت میشه. هر هفته ۵۰+ پروژه‌ داریم که داک‌هاشون آپدیت میشن ــ بدون هیچ زحمت مستندسازی دستی‌ای!

  • AIها بهتر شدند: Copilot و Claude براساس context واقعی تیم و پروژه، کد و خروجی می‌دادند.

آیا واقعاً می‌ارزید؟

برای شروع روی چند سرویس قدیمی و حتی متروکه تست کردیم. پروژه‌هایی که توسعه‌دهنده اصلی رفته، README قدیمیه و داک وجود نداره. هر کی بخواد تغییری بده باید کلی وقت بذاره تا بفهمه چی به چیه، و نتیجه خیلی برامون جذاب بود! داک‌های جدید، این سرویس‌ها از "جعبه سیاه" تبدیل شدن به سیستم‌های شناخته‌تر شده. کار توسعه جدید رو این سرویس‌ها کلی جلو افتاد و راحت تر شد!

یه اتفاق خوب دیگه‌ای که افتاد این بود وقتی سیستمی نوشتیم که کل پروژه‌های شرکت رو دوره‌ای تحلیل کنه و توی یک پوشه توی خود پروژه بریزه، دیگه حتی ابزار‌های تولید کد با AI، مثل Cursor و Aider، خیلی بهتر پروژه رو می فهمید و می تونست کد‌های درست، با کیفیت بالاتر، و مطابق استاندارد‌ها بزنه، اونم با هزینه‌ی کمتر! چون نیاز به کل پروژه برای فهم پروژه نیست.

خبر خوب اینکه دیوار تصمیم گرفته این پروژه رو اوپن سورس کنه و اگه کد پروژه رو می‌خواید ببینید یا اجراش کنید، اینجا کلیک کنید.