# راهنمای پیاده‌سازی RAG در Agno Agent ## مقدمه در این سند، به بررسی مشکل استفاده از گزینه `search_knowledge` در کلاس Agent فریمورک Agno و راه‌حل پیشنهادی برای آن می‌پردازیم. همچنین سیر تکامل پیاده‌سازی از رویکرد اولیه (Override کردن متدها) به رویکرد فعلی (استفاده از Pre-Hook) را شرح می‌دهیم. ## مشکل استفاده از `search_knowledge` ### توضیح مشکل وقتی از کلاس `Agent` در فریمورک Agno استفاده می‌کنیم، گزینه `search_knowledge` به مدل اجازه می‌دهد تا برای پیدا کردن جواب به صورت مستقیم به وکتور دیتابیس متصل شود و اطلاعات را جستجو کند. ```python # مثال استفاده از search_knowledge agent = Agent( model=model, knowledge=knowledge_base, search_knowledge=True, # این گزینه ممکن است با همه مدل‌ها سازگار نباشد # ... ) ``` ### چالش‌ها 1. **سازگاری مدل‌ها**: همه مدل‌های زبان از ابزارهای جستجو (tools/functions) پشتیبانی نمی‌کنند 2. **خطاهای احتمالی**: مدل‌هایی که از ابزارها پشتیبانی نمی‌کنند ممکن است در مواجه با دستورات جستجو دچار خطا شوند 3. **کنترل کمتر**: مدل به صورت خودکار تصمیم می‌گیرد چه زمانی جستجو کند ## رویکرد اولیه (منسوخ): Override کردن متدهای Agent ### توضیح رویکرد قدیمی در پیاده‌سازی اولیه، یک کلاس `ContextAwareAgent` ساخته بودیم که از `Agent` ارث‌بری می‌کرد و متدهای `run` و `print_response` را override می‌کرد: ```python # ⚠️ رویکرد قدیمی - دیگر استفاده نمی‌شود class ContextAwareAgent(Agent): def run(self, message, **kwargs): """Override run to always use RAG pipeline""" rag_prompt = build_rag_prompt(message) return super().run(rag_prompt, **kwargs) def print_response(self, message, **kwargs): """Override print_response to always use RAG pipeline""" rag_prompt = build_rag_prompt(message) return super().print_response(rag_prompt, **kwargs) ``` ### مشکلات رویکرد قدیمی 1. **خطر خراب شدن رفتار Agent**: با override کامل متدهای `run` و `print_response`، ممکن بود منطق داخلی فریمورک Agno (مثل guardrails، hooks، history management و ...) نادیده گرفته شود یا به شکل غیرمنتظره‌ای رفتار کند. 2. **وابستگی به ساختار داخلی**: هر تغییری در نسخه جدید Agno روی متدهای `run` یا `print_response` می‌توانست پیاده‌سازی ما را خراب کند. 3. **عدم ترکیب‌پذیری**: اضافه کردن منطق‌های دیگر (مثل sync config، guardrails و ...) به این کلاس، آن را پیچیده و شکننده می‌کرد. ## رویکرد فعلی: استفاده از Pre-Hook برای تزریق RAG ### چرا Pre-Hook؟ فریمورک Agno مکانیزم `pre_hooks` را فراهم کرده که اجازه می‌دهد **قبل از اجرای اصلی Agent**، روی ورودی کاربر تغییراتی اعمال کنیم. با این رویکرد: - **هیچ متدی override نمی‌شود** و رفتار اصلی Agent دست‌نخورده باقی می‌ماند - منطق RAG به صورت یک **تابع مستقل و قابل تست** پیاده‌سازی می‌شود - می‌توان چندین hook را **به صورت زنجیره‌ای** ترکیب کرد (مثلاً guardrail + config sync + RAG injection) ### مزایای این رویکرد نسبت به Override | ویژگی | Override متدها (قدیمی) | Pre-Hook (فعلی) | |--------|------------------------|-----------------| | ایمنی رفتار Agent | پایین - ممکن است منطق داخلی خراب شود | بالا - رفتار اصلی حفظ می‌شود | | سازگاری با نسخه‌های جدید Agno | شکننده | مقاوم | | ترکیب‌پذیری با سایر منطق‌ها | دشوار | آسان - فقط hook اضافه کنید | | قابلیت تست | متوسط | بالا - هر hook مستقلاً قابل تست است | | سازگاری مدل‌ها | بالا | بالا | | کنترل توسعه‌دهنده | بالا | بالا | | قابلیت debug | بالا | بالا | ## پیاده‌سازی فعلی ### ساختار فایل‌ها #### 1. `src/utils/hooks.py` - هسته اصلی Pre-Hook ها این فایل حاوی hook های مختلفی است که قبل از اجرای Agent فراخوانی می‌شوند: ```python from agno.run.agent import RunInput from src.utils.search_knowledge import build_rag_prompt def rag_injection_hook(run_input: RunInput, **kwargs): """ Intercepts the user input and injects RAG context. """ print("🪝 Hook: Injecting RAG Context...") # Modify the input content in place original_input = run_input.input_content run_input.input_content = build_rag_prompt(original_input) # Don't return anything - modifications are in-place ``` **نکته مهم**: این hook شیء `RunInput` را **در جا (in-place)** تغییر می‌دهد. یعنی `input_content` اصلی کاربر را با prompt غنی‌شده از RAG جایگزین می‌کند، بدون اینکه نیازی به بازگرداندن مقداری باشد. همچنین یک hook دیگر برای sync کردن تنظیمات agent از دیتابیس وجود دارد: ```python 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. """ agent = kwargs.get("agent") if not agent: return config = get_active_agent_config() if config and config.get("system_prompts"): new_prompts = config["system_prompts"] agent.instructions = new_prompts return run_input ``` #### 2. `src/utils/search_knowledge.py` - منطق RAG و جستجو این فایل حاوی تابع `build_rag_prompt` است که مسئولیت جستجوی دانش، reranking نتایج و ساخت prompt را بر عهده دارد: ```python def build_rag_prompt(user_question: str, embedder_model_name: str = None) -> str: """RAG pipeline با Qdrant - استفاده از سیستم embedding جدید""" global knowledge_base try: # Lazy initialization of knowledge base if knowledge_base is None: embed_factory = EmbeddingFactory() embedder = embed_factory.get_embedder(embedder_model_name) vector_db = get_qdrant_store(embedder=embedder) knowledge_base = Knowledge(vector_db=vector_db) # Search for relevant documents initial_results = knowledge_base.search(query=user_question, max_results=7) if not initial_results: context_str = "No information found in database." else: # Reranking: ارسال نتایج اولیه به Jina برای انتخاب بهترین‌ها relevant_docs = rerank_documents( query=user_question, documents=initial_results, top_n=3 ) # ساخت context با اطلاعات منبع و امتیاز context_parts = [] for doc in relevant_docs: meta = getattr(doc, "meta_data", {}) or {} source = meta.get('source', 'Unknown') score = meta.get('rerank_score', 0) content = f"[Source: {source} | Relevance: {score:.2f}]\n{doc.content}" context_parts.append(content) context_str = "\n\n".join(context_parts) except Exception as e: context_str = "Knowledge base temporarily unavailable." final_prompt = ( "Here is the context from the database:\n" "---------------------\n" f"{context_str}\n" "---------------------\n" f"User Question: {user_question}" ) return final_prompt ``` #### 3. `src/agents/base_agent.py` - تنظیمات Agent با Pre-Hook در این فایل، Agent استاندارد Agno با پارامتر `pre_hooks` ساخته می‌شود. دیگر از `ContextAwareAgent` استفاده نمی‌شود: ```python from agno.agent import Agent from src.utils.hooks import sync_config_hook, rag_injection_hook class IslamicScholarAgent: def __init__(self, model, knowledge_base, custom_instructions=None, db_url=None): # ... self.agent = Agent( name="Islamic Scholar Agent", model=model, instructions=self.custom_instructions, markdown=True, search_knowledge=False, db=PostgresDb(db_url=self.db_url) if self.db_url else None, add_history_to_context=True, reasoning=False, knowledge=None, # داده از طریق pre_hooks تزریق می‌شود debug_mode=False, pre_hooks=[ PromptInjectionGuardrail(), InputLimitGuardrail(), sync_config_hook, rag_injection_hook, ], ) ``` **نکته‌های کلیدی**: - `knowledge=None`: چون داده‌ها از طریق `rag_injection_hook` به prompt تزریق می‌شوند، نیازی به پاس دادن knowledge به Agent نیست. - `search_knowledge=False`: مدل نباید خودش تصمیم بگیرد چه زمانی جستجو کند. - `pre_hooks=[...]`: زنجیره‌ای از hook ها و guardrail ها که **به ترتیب** قبل از اجرای Agent اعمال می‌شوند. #### 4. `src/agents/islamic_scholar_agent.py` کلاس تخصصی که از `IslamicScholarAgent` پایه ارث‌بری می‌کند: ```python from .base_agent import IslamicScholarAgent as BaseIslamicScholarAgent class IslamicScholarAgent(BaseIslamicScholarAgent): """Specialized Islamic Scholar Agent""" def __init__(self, model, knowledge_base, custom_instructions=None, db_url=None): super().__init__(model, knowledge_base, custom_instructions, db_url) ``` ## منطق پیاده‌سازی ### 1. جریان کاری RAG با Pre-Hook ```mermaid graph TD A[سوال کاربر] --> B[pre_hooks اجرا می‌شوند] B --> B1[PromptInjectionGuardrail] B1 --> B2[InputLimitGuardrail] B2 --> B3[sync_config_hook] B3 --> B4[rag_injection_hook] B4 --> C[build_rag_prompt فراخوانی می‌شود] C --> D[جستجو در Qdrant - دریافت 7 نتیجه] D --> E{یافتن نتایج؟} E -->|بله| F[Reranking با Jina - انتخاب 3 نتیجه برتر] E -->|خیر| G[پیام عدم وجود اطلاعات] F --> H[ساخت prompt نهایی با منبع و امتیاز] G --> H H --> I[جایگزینی input_content در RunInput] I --> J[Agent اصلی Agno با prompt غنی‌شده اجرا می‌شود] J --> K[پاسخ مدل بر اساس context] ``` ### 2. ترتیب اجرای Pre-Hook ها وقتی کاربر یک سوال ارسال می‌کند، قبل از رسیدن به مدل، زنجیره زیر اجرا می‌شود: 1. **`PromptInjectionGuardrail`**: بررسی ورودی برای حملات prompt injection 2. **`InputLimitGuardrail`**: بررسی محدودیت طول ورودی 3. **`sync_config_hook`**: دریافت آخرین تنظیمات (system prompts) از دیتابیس Django و اعمال آن‌ها 4. **`rag_injection_hook`**: جستجو در وکتور دیتابیس و تزریق context به سوال کاربر ### 3. مراحل پیاده‌سازی RAG #### مرحله ۱: Lazy Initialization با EmbeddingFactory ```python if knowledge_base is None: embed_factory = EmbeddingFactory() embedder = embed_factory.get_embedder(embedder_model_name) vector_db = get_qdrant_store(embedder=embedder) knowledge_base = Knowledge(vector_db=vector_db) ``` #### مرحله ۲: جستجوی اولیه در Qdrant ```python # دریافت 7 نتیجه اولیه از وکتور دیتابیس initial_results = knowledge_base.search(query=user_question, max_results=7) ``` #### مرحله ۳: Reranking نتایج ```python # ارسال نتایج اولیه به Jina Reranker برای انتخاب 3 نتیجه برتر relevant_docs = rerank_documents( query=user_question, documents=initial_results, top_n=3 ) ``` #### مرحله ۴: ساخت Context با اطلاعات منبع ```python context_parts = [] for doc in relevant_docs: meta = getattr(doc, "meta_data", {}) or {} source = meta.get('source', 'Unknown') score = meta.get('rerank_score', 0) content = f"[Source: {source} | Relevance: {score:.2f}]\n{doc.content}" context_parts.append(content) context_str = "\n\n".join(context_parts) ``` #### مرحله ۵: ساخت Prompt نهایی و تزریق به RunInput ```python # در build_rag_prompt: final_prompt = ( "Here is the context from the database:\n" "---------------------\n" f"{context_str}\n" "---------------------\n" f"User Question: {user_question}" ) # در rag_injection_hook: run_input.input_content = build_rag_prompt(original_input) ``` ## مدیریت خطاها ### استراتژی مدیریت خطا ```python try: # منطق جستجو و reranking initial_results = knowledge_base.search(query=user_question, max_results=7) relevant_docs = rerank_documents(query=user_question, documents=initial_results, top_n=3) # ... except Exception as e: print(f"⚠️ Knowledge Base error (continuing without RAG): {e}") context_str = "Knowledge base temporarily unavailable. dont answer the question because you have no related information in the database." ``` ### مزایای این رویکرد 1. **مقاومت در برابر خطا**: سیستم با وجود مشکل در Knowledge Base ادامه کار می‌دهد 2. **شفافیت**: خطاها به صورت log ثبت می‌شوند 3. **graceful degradation**: در صورت مشکل، پیام مناسبی به مدل ارسال می‌شود تا از پاسخگویی بدون اطلاعات خودداری کند ## مزایای کلی رویکرد Pre-Hook ### مقایسه سه رویکرد | ویژگی | `search_knowledge=True` | Override متدها (قدیمی) | Pre-Hook (فعلی) | |--------|------------------------|----------------------|-----------------| | سازگاری مدل‌ها | محدود | بالا | بالا | | کنترل توسعه‌دهنده | کم | بالا | بالا | | ایمنی رفتار Agent | بالا | پایین | بالا | | ترکیب‌پذیری | کم | کم | بالا | | سازگاری با نسخه‌های جدید | متوسط | پایین | بالا | | قابلیت debug | متوسط | بالا | بالا | | انعطاف‌پذیری | کم | متوسط | بالا | | مدیریت خطا | خودکار | سفارشی | سفارشی | ### مزایای کلیدی 1. **ایمنی رفتار Agent**: هیچ متدی override نمی‌شود و رفتار اصلی Agno حفظ می‌شود 2. **ترکیب‌پذیری**: می‌توان چندین hook (guardrails، config sync، RAG) را به صورت زنجیره‌ای اجرا کرد 3. **سازگاری جهانی**: با همه مدل‌های زبان کار می‌کند 4. **مقاومت در برابر تغییرات فریمورک**: چون از API رسمی Agno (pre_hooks) استفاده می‌شود، با آپدیت‌های آینده سازگار خواهد بود 5. **قابل تست**: هر hook یک تابع مستقل است و به راحتی قابل unit test است 6. **شفافیت**: امکان مشاهده و debug هر مرحله از زنجیره hooks 7. **Reranking هوشمند**: نتایج اولیه از Qdrant با Jina Reranker فیلتر و رتبه‌بندی می‌شوند ## نکات پیاده‌سازی ### ۱. تنظیمات محیطی اطمینان حاصل کنید که متغیرهای محیطی زیر تنظیم شده‌اند: ```bash QDRANT_URL=your_qdrant_url DB_USER=your_db_user DB_PASSWORD=your_db_password DB_HOST=your_db_host DB_PORT=your_db_port DB_NAME=your_db_name ``` ### ۲. تنظیمات Collection نام collection در Qdrant به صورت اتوماتیک توسط `get_qdrant_store` مدیریت می‌شود. ### ۳. تنظیمات جستجو و Reranking - `max_results=7`: تعداد نتایج اولیه از Qdrant - `top_n=3`: تعداد نتایج نهایی پس از reranking با Jina - Reranker نتایج را بر اساس ارتباط معنایی با سوال کاربر رتبه‌بندی می‌کند ### ۴. زبان و Encoding - اطمینان از سازگاری encoding داده‌ها - پشتیبانی از زبان‌های مختلف (فارسی، عربی، انگلیسی) ## نتیجه‌گیری پیاده‌سازی RAG در این پروژه از سه مرحله تکاملی عبور کرده است: 1. **`search_knowledge=True`**: رویکرد پیش‌فرض Agno - محدود به مدل‌های خاص 2. **Override متدها (`ContextAwareAgent`)**: حل مشکل سازگاری مدل‌ها، اما با خطر خراب شدن رفتار Agent 3. **Pre-Hook (`rag_injection_hook`)**: رویکرد فعلی - ایمن، ترکیب‌پذیر و سازگار با فریمورک رویکرد فعلی Pre-Hook بهترین تعادل بین کنترل، ایمنی و انعطاف‌پذیری را فراهم می‌کند: - **بدون override**: رفتار اصلی Agent دست‌نخورده باقی می‌ماند - **زنجیره‌ای**: Guardrails، Config Sync و RAG Injection همگی به صورت مرتب و قابل مدیریت اجرا می‌شوند - **Reranking هوشمند**: با استفاده از Jina، کیفیت نتایج بازگشتی بهبود یافته - **مقاوم**: سیستم در صورت خطا در هر مرحله، به صورت graceful ادامه کار می‌دهد این پیاده‌سازی در فایل‌های موجود به خوبی کار می‌کند و می‌تواند به عنوان الگوی مناسبی برای پروژه‌های مشابه استفاده شود.