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.",
]
این پرامپت در دو جا استفاده میشود:
- هنگام ساخت Agent: به عنوان
instructionsاولیه - زمان عدم دسترسی به دیتابیس: وقتی
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
نحوه کار
- دریافت Agent: فریمورک Agno در
kwargsشیء agent را پاس میدهد - خواندن از دیتابیس: تابع
get_active_agent_config()فراخوانی میشود (شامل retry) - جایگزینی instructions: اگر پرامپتهایی از دیتابیس برگشت،
agent.instructionsکاملاً جایگزین میشود - حالت 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 برمیگردد، پرامپت پیشفرض حفظ میشود |
عملکرد و تاخیر
چرا تاخیر ناچیز است؟
- Connection Pool: اتصال به دیتابیس از pool گرفته میشود (بدون overhead ساخت اتصال جدید)
- کوئری ساده: یک
SELECTساده با فیلترWHEREروی فیلدهای ایندکسشده - داده کم: تعداد پرامپتها معمولاً کمتر از ۲۰ رکورد است
- pool_pre_ping: اتصالهای مُرده سریع شناسایی و جایگزین میشوند
برآورد زمانی
| عملیات | زمان تقریبی |
|---|---|
| گرفتن اتصال از pool | < 1ms |
| اجرای کوئری SELECT | 1-5ms |
| پردازش نتایج و اعمال | < 1ms |
| مجموع (حالت عادی) | 2-7ms |
| Retry (در صورت خطا) | +1000ms به ازای هر retry |
در مقایسه با زمان پاسخدهی LLM (معمولاً 1-10 ثانیه)، تاخیر 2-7 میلیثانیهای کاملاً قابل چشمپوشی است.
نحوه استفاده از پنل ادمین
افزودن پرامپت جدید
- وارد پنل ادمین Django شوید
- به بخش Agent Prompts بروید
- پرامپت جدید بسازید:
content: متن پرامپتsettings_id: شماره تنظیمات (معمولاً 1)is_active: تیک بزنید تا فعال باشد
- ذخیره کنید
تغییرات بلافاصله در درخواست بعدی کاربر اعمال میشوند.
غیرفعال کردن پرامپت
- تیک
is_activeرا بردارید - پرامپت از لیست ارسالی به Agent حذف میشود اما در دیتابیس باقی میماند
- میتوانید هر زمان دوباره فعالش کنید
تست رفتار جدید
- پرامپتها را در پنل ادمین تغییر دهید
- یک پیام تستی به Agent بفرستید
- پاسخ را بررسی کنید
- در صورت نیاز، پرامپتها را اصلاح کنید و دوباره تست کنید
این چرخه بدون هیچ ریدپلوی یا ریستارتی انجام میشود.
تنظیمات محیطی
| متغیر | توضیح | الزامی |
|---|---|---|
DB_USER |
نام کاربری دیتابیس PostgreSQL | بله |
DB_PASSWORD |
رمز عبور دیتابیس | بله |
DB_HOST |
آدرس سرور دیتابیس | بله |
DB_PORT |
پورت دیتابیس | بله |
DB_NAME |
نام دیتابیس | بله |
نتیجهگیری
سیستم پرامپت داینامیک با ترکیب سه مولفه ساده اما موثر کار میکند:
- دیتابیس + پنل ادمین: ذخیره و مدیریت آسان پرامپتها
get_active_agent_config: خواندن امن از دیتابیس با retry و fallbacksync_config_hook: اعمال پرامپتها روی Agent به صورت Pre-Hook
این معماری امکان تغییر لحظهای رفتار Agent را بدون نیاز به تغییر کد، ریدپلوی یا ریستارت فراهم میکند. تاخیر ناشی از خواندن دیتابیس در حد چند میلیثانیه است و در مقایسه با زمان پردازش LLM کاملاً قابل چشمپوشی است. همچنین سیستم Fallback تضمین میکند که حتی در صورت قطعی دیتابیس، Agent با پرامپت پیشفرض به کار خود ادامه میدهد.