جزئیات پست

تغییر فضای مسئله به یک مثال کوچکتر در تعامل با AI

 

تغییر فضای مسئله به یک مثال کوچکتر در تعامل با AI


بخش اول: داستان آشنا برای همه برنامه‌نویس‌ها

    دوستان برنامه‌نویس حتماً براتون پیش اومده که زمان زیادی با هوش مصنوعی در یک رفت/برگشت بی‌پایان قرار گرفتید و دست آخر قید کمک گرفتن از هوش مصنوعی رو زدید.

اگر پاسختان مثبت هست، برای خواندن این مقاله چند دقیقه وقت بذارید. قطعاً براتون راه‌گشا خواهد بود.


روش مرسوم

     خیلی وقت‌ها در تعامل با هوش مصنوعی، برای سریع‌تر رسیدن به جواب، کل کد رو در اختیارش می‌ذاریم. غالباً در پیدا کردن راه‌حل مناسب به نتیجه می‌رسیم. زحمت زیادی هم در شرح مشکلات و خواسته‌ها به خودمون نمی‌دیم. می‌گیم "اینو به این شکل درست کن" و تمام. خودش کد رو می‌خونه و بروزشده‌اش رو به ما تحویل می‌ده. در اکثر موارد، این روش به خوبی جواب می‌دهد و هیچ ایرادی هم به آن نیست.


مشکل کجا پیش می‌آید؟

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


وقتی به بن‌بست می‌رسیم

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


راهبرد جدید: تغییر اندازه فضای مسئله

    اینجا جایی است که باید راهبرد و استراتژی خودما را عوض کنیم. به‌طور خلاصه، لازم است که اندازه فضای مسئله را تغییر دهیم.

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


فضای بزرگ چه اشکالی دارد؟

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


پروتوتایپ به چه شکل باشد؟

    در نظر داشته باشیم که در روش مرسوم ما همیشه نقشه کل شهر با تمام امکانات را به هوش مصنوعی می‌دیم. حالا می‌آییم و نقشه یک مجتمع کوچک چند واحدی را به او می‌دهیم. و می‌دانیم که جواب ما در هر دو فضا یکی است.

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

این فضا/مثال چه خصوصیاتی دارد؟

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


نتیجه تعامل با پروتوتایپ

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


انتقال راه‌حل به پروژه اصلی

     وقتی راه‌حل پیدا شد، مثال و مدل فضای مسئله را دوباره به سورس کد اصلی منتقل می‌کنیم و می‌گوییم راه‌حلی که پیدا کردید را بر اساس آن منطبق و اعمال کن.

     یک ذره زمان می‌برد، ولی در مقابل آن همه زمانی که در راهبرد مرسوم به هدر رفته، اصلاً به نظر نمی‌آید.


نکته پایانی

معمولاً برنامه‌نویسان وابسته به هوش مصنوعی از این راه‌کار گریزان هستند، چون به تعاملات کوتاه عادت کرده‌اند و از ارائه توضیحات کامل، آن هم در حد ساخت یک مثال کوچک، فرار می‌کنند. اما در نهایت، برای رسیدن به جواب درست مجبور هستند از این روش استفاده کنند.

پس اگر دیدی داری وارد یک دور باطل رفت/برگشت با هوش مصنوعی می‌شوی و زمان هم نداری، دیگر وقت را تلف نکن. یک مثال ساده به شکل پروتوتایپ/سودوکد از مشکلت بزن تا سریع به جواب برسی.


بخش دوم: یک مثال عملی (جنریک کردن سرویس‌ها)

حالا که با استراتژی آشنا شدید، بیایید یک مثال واقعی از دنیای برنامه‌نویسی را با هم مرور کنیم.


سناریو: وقتی سرویس‌های تکراری، طاقت‌فرسا می‌شوند

فرض کنید در یک پروژه نسبتاً بزرگ، چندین سرویس دارید که کارهای مشابهی انجام می‌دهند:

  • سرویس مدیریت کاربران (UserService)
  • سرویس مدیریت محصولات (ProductService)
  • سرویس مدیریت سفارشات (OrderService)
  • سرویس مدیریت دسته‌بندی‌ها (CategoryService)

هر کدام از این سرویس‌ها، متدهای تقریباً یکسانی دارند:

csharp

GetById(int id)    // دریافت با شناسه
GetAll()           // دریافت لیست همه
Create(entity)     // ایجاد جدید
Update(entity)     // بروزرسانی
Delete(int id)     // حذف


اولین قدم (جنریک کردن سرویس‌ها):

به‌جای اینکه برای هر سرویس، این متدها را جداگانه بنویسیم (که باعث تکراری شدن کد می‌شود)، یک سرویس جنریک طراحی می‌کنیم:

csharp

public interface IGenericService<T> where T : class
{
    Task<T> GetById(int id);
    Task<List<T>> GetAll();
    Task<T> Create(T entity);
    Task<T> Update(T entity);
    Task<bool> Delete(int id);
}

و بعد:

csharp

public class GenericService<T> : IGenericService<T> where T : class
{
    protected readonly DbContext _context;
    protected readonly DbSet<T> _dbSet;
 
    public GenericService(DbContext context)
    {
      _context = context;
      _dbSet = context.Set<T>();
    }
 
    public virtual async Task<T> GetById(int id)
    {
        return await _dbSet.FindAsync(id);
    }
 
    public virtual async Task<List<T>> GetAll()
    {
        return await _dbSet.ToListAsync();
    }
 
    public virtual async Task<T> Create(T entity)
    {
        await _dbSet.AddAsync(entity);
        await _context.SaveChangesAsync();
        return entity;
    }
 
    public virtual async Task<T> Update(T entity)
    {
      _dbSet.Update(entity);
        await _context.SaveChangesAsync();
        return entity;
    }
 
    public virtual async Task<bool> Delete(int id)
    {
        var entity = await GetById(id);
        if (entity == null) return false;
      _dbSet.Remove(entity);
        await _context.SaveChangesAsync();
        return true;
    }
}

حالا به‌جای هر سرویس، فقط کافی است:

csharp

public class UserService : GenericService<User>
{
    public UserService(DbContext context) : base(context) { }
    // فقط متدهای اختصاصی User اینجا می‌آیند
}


چالش اصلی (نیاز به سفارشی‌سازی متدها):

همه چیز خوب پیش می‌رود، تا اینکه یک نیاز جدید مطرح می‌شود:

"ما می‌خواهیم بعضی از متدها را بتوانیم سفارشی‌سازی کنیم. مثلاً متد GetAll  برای بعضی موجودیت‌ها باید با فیلتر خاصی بیاید. برای بعضی دیگر باید مرتب‌سازی داشته باشد. برای یک سری باید شامل اطلاعات وابسته (Include) شود."

به عبارت دیگر، می‌خواهیم قابلیت‌های بیشتری به سرویس جنریک اضافه کنیم، بدون اینکه ساختار کلی را بشکنیم.


روش مرسوم (که جواب نمی‌دهد)

طبق روال همیشگی، کل سورس کد را به هوش مصنوعی می‌دهیم و می‌گوییم:

"این سرویس جنریک را طوری تغییر بده که بشود متدها را سفارشی‌سازی کرد."

هوش مصنوعی راه‌حل‌های مختلفی پیشنهاد می‌دهد:

راه‌حل اول (اضافه کردن پارامترهای اختیاری):

csharp

Task<List<T>> GetAll(
    Expression<Func<T, bool>>? filter = null,
    Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null,
    params Expression<Func<T, object>>[] includes
);

 اما این باعث می‌شود متدها بیش از حد شلوغ شوند و استفاده از آنها سخت گردد.

راه‌حل دوم (استفاده ازSpecification Patter):

csharp

Task<List<T>> GetAll(ISpecification<T> spec);

       اما این نیاز به تغییرات اساسی در کل پروژه دارد و خیلی از قسمت‌ها را تحت تأثیر قرار می‌دهد.

راه‌حل سوم (جدا کردن Query ازCommand):

csharp

public interface IQueryService<T> { ... }
public interface ICommandService<T> { ... }

  این هم ساختار را پیچیده می‌کند و همه‌جا را تحت تأثیر قرار می‌دهد.


نتیجه روش مرسوم:

    هر بار که هوش مصنوعی یک راه‌حل می‌دهد، بعد از اعمال به کل پروژه، متوجه می‌شویم که:

  • یا خیلی از قسمت‌های دیگر پروژه خراب می‌شوند
  • یا راه‌حل آنقدر عمومی است که نیازهای خاص را پوشش نمی‌دهد
  • یا ساختار آنقدر پیچیده می‌شود که استفاده از سرویس جنریک دیگر راحت نیست

رفت/برگشت، رفت/برگشت... و در نهایت قید می‌کنیم.


راهبرد جدید: ساخت پروتوتایپ:

به‌جای کل سورس کد، یک پروتوتایپ ساده درست می‌کنیم. فقط با دو مدل ساده:

csharp

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsActive { get; set; }
}
 
public class Product
{
    public int Id { get; set; }
    public string Title { get; set; }
    public decimal Price { get; set; }
}

و یک سرویس جنریک ساده که فقط متد GetAll را دارد:

csharp

public interface IGenericService<T>
{
    Task<List<T>> GetAll();
}
 
public class GenericService<T> : IGenericService<T>
{
    protected readonly List<T> _data; // شبیه‌سازی دیتابیس در حافظه
 
    public GenericService(List<T> data)
    {
      _data = data;
    }
 
    public virtual Task<List<T>> GetAll()
    {
        return Task.FromResult(_data.ToList());
    }
}


تعامل با هوش مصنوعی روی پروتوتایپ:

    حالا با یک مثال کوچک و شفاف، به هوش مصنوعی می‌گوییم:

      "من می‌خواهم متد GetAll  را طوری طراحی کنم که برای User، فقط کاربران فعال را برگرداند، ولی برای Product، محصولات با قیمت بالای ۱۰۰ را نشان دهد. چطور می‌توانم این قابلیت را به سرویس جنریک اضافه کنم، بدون اینکه ساختار اصلی را خراب کنم؟"


تعامل اول:

    ما: آیا می‌توانم یک Func<T, bool> به متد اضافه کنم؟

     هوش مصنوعی: بله، ولی اینطوری هر بار باید فیلتر را به متد پاس بدهی و این کار استفاده از سرویس را برای توسعه‌دهنده سخت می‌کند.


تعامل دوم:

    ما: چطور می‌توانم فیلترها را به‌صورت پیش‌فرض برای هر موجودیت تعریف کنم؟

هوش مصنوعی: می‌توانی از Dictionary<Type, Expression<Func<T, bool>>>  یا یک BaseSpecification<T>  استفاده کنی، اما این کار پیچیدگی بیشتری به پروژه اضافه می‌کند.


تعامل سوم:

    ما: راه ساده‌تری نیست؟ یک چیزی مثل ارث‌بری که هر سرویس بتواند متد را override کند؟

     هوش مصنوعی: دقیقاً! این بهترین راه‌حل است. متد را virtual  کن و در سرویس‌های فرزند، فقط متد مورد نظر را بازنویسی کن.


راه‌حل نهایی:

csharp

public class GenericService<T> : IGenericService<T>
{
    protected readonly List<T> _data;
 
    public GenericService(List<T> data)
    {
      _data = data;
    }
 
    public virtual Task<List<T>> GetAll()
    {
        return Task.FromResult(_data.ToList());
    }
}
 
// سرویس کاربران با فیلتر سفارشی
public class UserService : GenericService<User>
{
    public UserService(List<User> data) : base(data) { }
 
    public override Task<List<User>> GetAll()
    {
        // فقط کاربران فعال
        var filtered = _data.Where(u => u.IsActive).ToList();
        return Task.FromResult(filtered);
    }
}
 
// سرویس محصولات با فیلتر سفارشی
public class ProductService : GenericService<Product>
{
    public ProductService(List<Product> data) : base(data) { }
 
    public override Task<List<Product>> GetAll()
    {
        // فقط محصولات با قیمت بالای ۱۰۰
        var filtered = _data.Where(p => p.Price > 100).ToList();
        return Task.FromResult(filtered);
    }
}


انتقال به پروژه اصلی:

     حالا که راه‌حل روی نمونه کوچک به‌درستی کار کرد، آن را به کل پروژه منتقل می‌کنیم:

"همین الگویی که روی نمونه کوچک پیدا کردیم (متدهای virtual  و override)، روی کل سورس کد پیاده‌سازی کن."

و نتیجه می‌گیریم:

  • ساختار سرویس جنریک سالم ماند
  • هر سرویس فرزند توانست متدهای مورد نظر خود را سفارشی‌سازی کند
  • هیچ بخش دیگری از پروژه تحت تأثیر قرار نگرفت
  • کد تمیز، خوانا و قابل نگهداری باقی ماند

جمع‌بندی نهایی:

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

     دفعه بعد که در یک رفت/برگشت بی‌نتیجه با هوش مصنوعی گیر کردید، این روش را امتحان کنید. مطمئن باشید نتیجه‌اش شما را شگفت‌زده خواهد کرد.


موفق و پیروز باشید! 🚀