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

راهنمای پیاده‌سازی RAG در Agno Agent

مقدمه

در این سند، به بررسی مشکل استفاده از گزینه search_knowledge در کلاس Agent فریمورک Agno و راه‌حل پیشنهادی برای آن می‌پردازیم. همچنین سیر تکامل پیاده‌سازی از رویکرد اولیه (Override کردن متدها) به رویکرد فعلی (استفاده از Pre-Hook) را شرح می‌دهیم.

مشکل استفاده از search_knowledge

توضیح مشکل

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

# مثال استفاده از 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 می‌کرد:

# ⚠️ رویکرد قدیمی - دیگر استفاده نمی‌شود
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 فراخوانی می‌شوند:

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 از دیتابیس وجود دارد:

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 را بر عهده دارد:

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 استفاده نمی‌شود:

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 پایه ارث‌بری می‌کند:

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

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

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

# دریافت 7 نتیجه اولیه از وکتور دیتابیس
initial_results = knowledge_base.search(query=user_question, max_results=7)

مرحله ۳: Reranking نتایج

# ارسال نتایج اولیه به Jina Reranker برای انتخاب 3 نتیجه برتر
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)

مرحله ۵: ساخت Prompt نهایی و تزریق به RunInput

# در 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)

مدیریت خطاها

استراتژی مدیریت خطا

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 فیلتر و رتبه‌بندی می‌شوند

نکات پیاده‌سازی

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

اطمینان حاصل کنید که متغیرهای محیطی زیر تنظیم شده‌اند:

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 ادامه کار می‌دهد

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