You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

18 KiB

سیستم پرامپت داینامیک: مدیریت رفتار Agent از طریق دیتابیس

مقدمه

یکی از چالش‌های اصلی در توسعه و تنظیم Agent ها، نیاز به تغییر مکرر System Prompt است. در حالت معمول، برای تغییر رفتار Agent باید کد را تغییر داد، ریدپلوی کرد و منتظر ماند. این فرایند برای تست و بهینه‌سازی رفتار Agent بسیار کُند و وقت‌گیر است.

ما این مشکل را با یک سیستم پرامپت داینامیک حل کرده‌ایم: System Prompt ها در دیتابیس (PostgreSQL) ذخیره می‌شوند و از طریق پنل ادمین Django قابل مدیریت هستند. یک Pre-Hook در هر درخواست کاربر، آخرین پرامپت‌ها را از دیتابیس می‌خواند و روی Agent اعمال می‌کند.

مزایای کلیدی

  • بدون ریدپلوی: تغییر رفتار Agent بدون نیاز به تغییر کد یا ریستارت سرویس
  • تست آسان: امکان آزمایش سریع پرامپت‌های مختلف از طریق پنل ادمین
  • تاخیر ناچیز: چون به صورت Pre-Hook اجرا می‌شود، overhead قابل چشم‌پوشی است
  • Fallback ایمن: در صورت عدم دسترسی به دیتابیس، پرامپت پیش‌فرض اعمال می‌شود

معماری

نمای کلی

graph TD
    A["پنل ادمین Django"] -->|مدیریت پرامپت‌ها| B["جدول agent_agentprompt"]
    
    C["درخواست کاربر"] --> D["Agent pre_hooks"]
    D --> E["sync_config_hook"]
    E -->|کوئری| B
    B -->|پرامپت‌های فعال| E
    E -->|agent.instructions = new_prompts| F["Agent"]
    
    E -->|خطای اتصال| G["retry 3 بار"]
    G -->|موفق| E
    G -->|ناموفق| H["default_system_prompt"]
    H --> F

جریان داده

پنل ادمین Django
    │
    │  CRUD عملیات (ایجاد/ویرایش/حذف/فعال‌سازی)
    ▼
┌─────────────────────────────────┐
│  جدول: agent_agentprompt        │
│  ────────────────────────────── │
│  id | settings_id | content     │
│     | is_active   | order       │
└───────────────┬─────────────────┘
                │
                │  SELECT (هر درخواست کاربر)
                ▼
┌─────────────────────────────────┐
│  sync_config_hook               │  ← src/utils/hooks.py
│  ────────────────────────────── │
│  1. دریافت پرامپت‌های فعال      │
│  2. جایگزینی agent.instructions │
└───────────────┬─────────────────┘
                │
                ▼
┌─────────────────────────────────┐
│  Agent با پرامپت‌های جدید       │
│  اجرای درخواست کاربر           │
└─────────────────────────────────┘

پیاده‌سازی

ساختار فایل‌ها

فایل مسئولیت
src/utils/load_settings.py اتصال به دیتابیس، خواندن پرامپت‌ها، مدیریت retry و تعریف پرامپت پیش‌فرض
src/utils/hooks.py Pre-Hook که پرامپت‌ها را از دیتابیس گرفته و روی Agent اعمال می‌کند
src/agents/base_agent.py تنظیم Agent با pre_hooks و پرامپت پیش‌فرض اولیه

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

فایل: src/utils/load_settings.py

تنظیم Connection Pool

from sqlalchemy import create_engine, text
from sqlalchemy.exc import OperationalError

engine = create_engine(
    db_url,
    pool_pre_ping=True,   # بررسی زنده بودن اتصال قبل از استفاده
    pool_recycle=3600,     # بستن و بازکردن اتصالات قدیمی‌تر از 1 ساعت
    pool_size=10,          # حداکثر 10 اتصال همزمان
    max_overflow=20        # اتصالات اضافی در ترافیک بالا
)

چرا pool_pre_ping=True؟

بدون این تنظیم، اگر اتصال دیتابیس به دلیل timeout یا مشکلات شبکه قطع شده باشد، اولین درخواست بعد از قطعی با خطا مواجه می‌شود. pool_pre_ping قبل از هر استفاده، یک ping ساده به دیتابیس ارسال می‌کند و در صورت قطعی، اتصال جدید می‌سازد.

خواندن پرامپت‌ها با Retry

def get_active_agent_config(retries=3, delay=1):
    """
    Fetches active prompts with automatic retry logic for database "hiccups".
    """
    attempt = 0
    while attempt < retries:
        try:
            with engine.connect() as conn:
                query = text("""
                    SELECT content 
                    FROM agent_agentprompt 
                    WHERE settings_id = 1 AND is_active = true 
                    ORDER BY id ASC
                """)
                result = conn.execute(query).fetchall()
                prompt_list = [row.content for row in result]
                return {"system_prompts": prompt_list}

        except OperationalError as e:
            attempt += 1
            print(f"⚠️ DB Connection error (Attempt {attempt}/{retries}): {e}")
            if attempt < retries:
                time.sleep(delay)
            else:
                print("❌ DB Retry limit reached.")
                return None

        except Exception as e:
            print(f"❌ Unexpected Error: {e}")
            return None

ساختار کوئری

SELECT content 
FROM agent_agentprompt 
WHERE settings_id = 1 AND is_active = true 
ORDER BY id ASC
فیلد توضیح
content متن پرامپت
settings_id = 1 فقط پرامپت‌های مربوط به تنظیمات فعلی
is_active = true فقط پرامپت‌های فعال (غیرفعال‌ها نادیده گرفته می‌شوند)
ORDER BY id ASC حفظ ترتیب تعریف شده پرامپت‌ها

از طریق پنل ادمین می‌توان:

  • پرامپت جدید اضافه کرد
  • پرامپت موجود را ویرایش کرد
  • یک پرامپت را غیرفعال (is_active = false) کرد بدون حذف آن
  • پرامپت را حذف کرد
  • ترتیب پرامپت‌ها را تغییر داد

مکانیزم Retry

سیستم retry سه‌مرحله‌ای برای مقابله با مشکلات موقت دیتابیس:

تلاش ۱ → خطا → صبر 1 ثانیه
    تلاش ۲ → خطا → صبر 1 ثانیه
        تلاش ۳ → خطا → برگشت None (استفاده از پرامپت پیش‌فرض)
پارامتر مقدار توضیح
retries 3 حداکثر تعداد تلاش
delay 1 ثانیه فاصله بین تلاش‌ها

نکته: فقط OperationalError (خطاهای اتصال/شبکه) باعث retry می‌شود. خطاهای دیگر (مثل خطای SQL) بلافاصله None برمی‌گردانند چون retry کردن آن‌ها فایده‌ای ندارد.

پرامپت پیش‌فرض (Fallback)

def default_system_prompt():
    return [
        "You are a strict Islamic Knowledge Assistant.",
        "Your Goal: Answer the user's question using the provided 'Context from the database'.",
        "STRICT BEHAVIORAL RULE: You must maintain the highest standard of Adab (Etiquette).",
        "If the user is disrespectful, vulgar, uses profanity, or mocks Islam:",
        "1. Do NOT engage with the toxicity.",
        "2. Do NOT lecture them.",
        "3. Refuse to answer immediately by saying: 'I cannot answer this due to violations of Adab.'",
        "If the Context is in a different language than the User's Question, you MUST translate ...",
        "If the answer is explicitly found in the context, answer directly.",
        "If the answer is NOT found in the context, strictly reply: 'Information not available ...'",
        "Maintain a respectful, scholarly tone.",
        "Do not explain your reasoning process in the final output.",
    ]

این پرامپت در دو جا استفاده می‌شود:

  1. هنگام ساخت Agent: به عنوان instructions اولیه
  2. زمان عدم دسترسی به دیتابیس: وقتی sync_config_hook نتواند پرامپت از دیتابیس بخواند، Agent با همین پرامپت پیش‌فرض کار می‌کند

بخش ۲: Pre-Hook برای اعمال پرامپت

فایل: src/utils/hooks.py

def sync_config_hook(run_input: RunInput, **kwargs):
    """
    Agno Pre-Hook: Fetches the latest Django DB config and 
    injects it into the agent before the run starts.
    """
    # 1. دسترسی به instance ایجنت
    agent = kwargs.get("agent")
    if not agent:
        return

    # 2. دریافت تنظیمات از دیتابیس
    config = get_active_agent_config()
    
    # 3. اعمال پرامپت‌های جدید
    if config and config.get("system_prompts"):
        new_prompts = config["system_prompts"]
        agent.instructions = new_prompts

    return run_input

نحوه کار

  1. دریافت Agent: فریمورک Agno در kwargs شیء agent را پاس می‌دهد
  2. خواندن از دیتابیس: تابع get_active_agent_config() فراخوانی می‌شود (شامل retry)
  3. جایگزینی instructions: اگر پرامپت‌هایی از دیتابیس برگشت، agent.instructions کاملاً جایگزین می‌شود
  4. حالت Fallback: اگر config برابر None باشد (خطای دیتابیس)، شرط if اجرا نمی‌شود و Agent با instructions قبلی (پیش‌فرض) کار می‌کند

نکته مهم: agent.instructions کاملاً overwrite می‌شود (نه append). یعنی لیست پرامپت‌های دیتابیس جایگزین کامل لیست قبلی می‌شود. این طراحی عمدی است تا پنل ادمین کنترل کامل روی رفتار Agent داشته باشد.

بخش ۳: ثبت Hook در Agent

فایل: src/agents/base_agent.py

from src.utils.hooks import sync_config_hook, rag_injection_hook
from src.utils.load_settings import default_system_prompt

class IslamicScholarAgent:
    def __init__(self, model, knowledge_base, custom_instructions=None, db_url=None):
        self.custom_instructions = custom_instructions or default_system_prompt()

        self.agent = Agent(
            name="Islamic Scholar Agent",
            model=model,
            instructions=self.custom_instructions,  # پرامپت اولیه (پیش‌فرض)
            pre_hooks=[
                PromptInjectionGuardrail(),
                InputLimitGuardrail(),
                sync_config_hook,       # ← اینجا پرامپت از دیتابیس خوانده و اعمال می‌شود
                rag_injection_hook,
            ],
            # ...
        )

ترتیب اجرای Hook ها

درخواست کاربر
    │
    ├─ 1. PromptInjectionGuardrail  → بررسی امنیتی ورودی
    ├─ 2. InputLimitGuardrail       → بررسی محدودیت طول ورودی
    ├─ 3. sync_config_hook          → خواندن پرامپت از دیتابیس و اعمال
    ├─ 4. rag_injection_hook        → تزریق context از وکتور دیتابیس
    │
    ▼
  Agent اجرا می‌شود (با پرامپت‌های تازه از دیتابیس + context از RAG)

sync_config_hook عمداً قبل از rag_injection_hook قرار دارد. ابتدا رفتار Agent (instructions) تنظیم می‌شود، سپس داده‌ها (context) به ورودی اضافه می‌شوند.

زنجیره Fallback کامل

graph TD
    A["sync_config_hook اجرا می‌شود"] --> B["get_active_agent_config فراخوانی"]
    B --> C{"تلاش ۱: اتصال به DB"}
    C -->|موفق| D["پرامپت‌های فعال برگشت"]
    C -->|OperationalError| E{"تلاش ۲"}
    E -->|موفق| D
    E -->|OperationalError| F{"تلاش ۳"}
    F -->|موفق| D
    F -->|OperationalError| G["return None"]
    
    D --> H["agent.instructions = پرامپت‌های دیتابیس"]
    G --> I["شرط if اجرا نمی‌شود"]
    I --> J["Agent با instructions فعلی کار می‌کند"]
    J --> K["default_system_prompt"]
    H --> L["Agent با پرامپت‌های جدید اجرا می‌شود"]

سناریوهای مختلف

سناریو نتیجه
دیتابیس در دسترس، پرامپت‌های فعال موجود پرامپت‌های دیتابیس اعمال می‌شوند
دیتابیس در دسترس، هیچ پرامپت فعالی نیست لیست خالی برمی‌گردد، شرط if رد می‌شود، پرامپت پیش‌فرض حفظ می‌شود
دیتابیس موقتاً قطع (تلاش ۱ یا ۲ موفق) پس از retry موفق، پرامپت‌های دیتابیس اعمال می‌شوند
دیتابیس کاملاً قطع (هر ۳ تلاش ناموفق) None برمی‌گردد، پرامپت پیش‌فرض حفظ می‌شود
خطای غیرمنتظره (مثلاً خطای SQL) بلافاصله None برمی‌گردد، پرامپت پیش‌فرض حفظ می‌شود

عملکرد و تاخیر

چرا تاخیر ناچیز است؟

  1. Connection Pool: اتصال به دیتابیس از pool گرفته می‌شود (بدون overhead ساخت اتصال جدید)
  2. کوئری ساده: یک SELECT ساده با فیلتر WHERE روی فیلدهای ایندکس‌شده
  3. داده کم: تعداد پرامپت‌ها معمولاً کمتر از ۲۰ رکورد است
  4. pool_pre_ping: اتصال‌های مُرده سریع شناسایی و جایگزین می‌شوند

برآورد زمانی

عملیات زمان تقریبی
گرفتن اتصال از pool < 1ms
اجرای کوئری SELECT 1-5ms
پردازش نتایج و اعمال < 1ms
مجموع (حالت عادی) 2-7ms
Retry (در صورت خطا) +1000ms به ازای هر retry

در مقایسه با زمان پاسخ‌دهی LLM (معمولاً 1-10 ثانیه)، تاخیر 2-7 میلی‌ثانیه‌ای کاملاً قابل چشم‌پوشی است.

نحوه استفاده از پنل ادمین

افزودن پرامپت جدید

  1. وارد پنل ادمین Django شوید
  2. به بخش Agent Prompts بروید
  3. پرامپت جدید بسازید:
    • content: متن پرامپت
    • settings_id: شماره تنظیمات (معمولاً 1)
    • is_active: تیک بزنید تا فعال باشد
  4. ذخیره کنید

تغییرات بلافاصله در درخواست بعدی کاربر اعمال می‌شوند.

غیرفعال کردن پرامپت

  • تیک is_active را بردارید
  • پرامپت از لیست ارسالی به Agent حذف می‌شود اما در دیتابیس باقی می‌ماند
  • می‌توانید هر زمان دوباره فعالش کنید

تست رفتار جدید

  1. پرامپت‌ها را در پنل ادمین تغییر دهید
  2. یک پیام تستی به Agent بفرستید
  3. پاسخ را بررسی کنید
  4. در صورت نیاز، پرامپت‌ها را اصلاح کنید و دوباره تست کنید

این چرخه بدون هیچ ریدپلوی یا ریستارتی انجام می‌شود.

تنظیمات محیطی

متغیر توضیح الزامی
DB_USER نام کاربری دیتابیس PostgreSQL بله
DB_PASSWORD رمز عبور دیتابیس بله
DB_HOST آدرس سرور دیتابیس بله
DB_PORT پورت دیتابیس بله
DB_NAME نام دیتابیس بله

نتیجه‌گیری

سیستم پرامپت داینامیک با ترکیب سه مولفه ساده اما موثر کار می‌کند:

  1. دیتابیس + پنل ادمین: ذخیره و مدیریت آسان پرامپت‌ها
  2. get_active_agent_config: خواندن امن از دیتابیس با retry و fallback
  3. sync_config_hook: اعمال پرامپت‌ها روی Agent به صورت Pre-Hook

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