الدرس 18 من 20

بناء وكيل بحث

المنطق الأساسي

3 دقيقة للقراءة

الآن لننفذ عقل الوكيل—حلقة ReAct التي تستدل، وتتصرف، وتجمع نتائج البحث.

قوالب المطالبات

# prompts/templates.py

SYSTEM_PROMPT = """أنت مساعد بحث يساعد المستخدمين على فهم المواضيع بشكل شامل.

لديك وصول لهذه الأدوات:
{tools}

عند البحث، اتبع هذه الخطوات:
1. قسّم الموضوع إلى أسئلة رئيسية
2. ابحث عن معلومات للإجابة على كل سؤال
3. اجمع النتائج في سرد متماسك
4. استشهد دائماً بمصادرك

استخدم هذا التنسيق:
فكرة: [استدلالك حول ما يجب فعله بعد ذلك]
إجراء: [اسم_الأداة]
مدخل الإجراء: [استعلام للأداة]

بعد جمع معلومات كافية، قدم إجابتك النهائية بـ:
الإجابة النهائية: [ردك الشامل مع الاستشهادات]
"""

SYNTHESIS_PROMPT = """بناءً على نتائج البحث التالية، اكتب تقريراً شاملاً عن "{topic}".

النتائج:
{findings}

المتطلبات:
- ابدأ بنظرة عامة موجزة
- نظّم في أقسام منطقية
- ضمّن حقائق وبيانات محددة
- استشهد بالمصادر باستخدام [1]، [2]، إلخ.
- اختم بالنقاط الرئيسية
- احتفظ بأقل من {max_length} كلمة

المصادر:
{sources}
"""

مخزن الذاكرة

# memory/store.py
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime

@dataclass
class ResearchFinding:
    query: str
    content: str
    source_url: str
    source_title: str
    timestamp: datetime

class ResearchMemory:
    def __init__(self):
        self.findings: List[ResearchFinding] = []
        self.queries_made: set = set()

    def add_finding(self, query: str, content: str, url: str, title: str):
        finding = ResearchFinding(
            query=query,
            content=content,
            source_url=url,
            source_title=title,
            timestamp=datetime.now()
        )
        self.findings.append(finding)
        self.queries_made.add(query)

    def get_all_findings(self) -> str:
        return "\n\n".join([
            f"الاستعلام: {f.query}\n"
            f"المصدر: {f.source_title}\n"
            f"المحتوى: {f.content}"
            for f in self.findings
        ])

    def get_sources(self) -> List[dict]:
        seen = set()
        sources = []
        for i, f in enumerate(self.findings, 1):
            if f.source_url not in seen:
                sources.append({
                    "id": i,
                    "title": f.source_title,
                    "url": f.source_url
                })
                seen.add(f.source_url)
        return sources

    def has_searched(self, query: str) -> bool:
        return query.lower() in {q.lower() for q in self.queries_made}

فئة الوكيل الرئيسية

# agent.py
import re
from langchain_openai import ChatOpenAI
from tools.search import WebSearchTool
from memory.store import ResearchMemory
from prompts.templates import SYSTEM_PROMPT, SYNTHESIS_PROMPT
from config import Config

class ResearchAgent:
    def __init__(self, config: Config):
        self.config = config
        self.llm = ChatOpenAI(
            model=config.MODEL_NAME,
            temperature=config.TEMPERATURE,
            api_key=config.OPENAI_API_KEY
        )
        self.tools = {
            "web_search": WebSearchTool()
        }
        self.memory = None

    def research(self, topic: str) -> str:
        """نقطة الدخول الرئيسية للبحث"""
        self.memory = ResearchMemory()

        # تشغيل حلقة ReAct
        self._research_loop(topic)

        # تجميع النتائج
        report = self._synthesize_report(topic)

        return report

    def _research_loop(self, topic: str):
        """حلقة ReAct لجمع المعلومات"""
        tools_desc = "\n".join([
            f"- {name}: {tool.description}"
            for name, tool in self.tools.items()
        ])

        messages = [
            {"role": "system", "content": SYSTEM_PROMPT.format(tools=tools_desc)},
            {"role": "user", "content": f"ابحث في هذا الموضوع بشكل شامل: {topic}"}
        ]

        for iteration in range(self.config.MAX_ITERATIONS):
            response = self.llm.invoke(messages)
            content = response.content

            # التحقق من الإجابة النهائية
            if "الإجابة النهائية:" in content or "Final Answer:" in content:
                break

            # تحليل الإجراء
            action_match = re.search(
                r"Action:\s*(\w+)\s*\nAction Input:\s*(.+?)(?=\n|$)",
                content,
                re.DOTALL
            )

            if action_match:
                tool_name = action_match.group(1).strip()
                tool_input = action_match.group(2).strip()

                # تنفيذ الأداة
                if tool_name in self.tools:
                    result = self.tools[tool_name].run(tool_input)

                    # تخزين النتائج في الذاكرة
                    if result["success"]:
                        for r in result["results"]:
                            self.memory.add_finding(
                                query=tool_input,
                                content=r["snippet"],
                                url=r["url"],
                                title=r["title"]
                            )

                    # إضافة الملاحظة للرسائل
                    observation = f"ملاحظة: {self._format_results(result)}"
                    messages.append({"role": "assistant", "content": content})
                    messages.append({"role": "user", "content": observation})
                else:
                    messages.append({
                        "role": "user",
                        "content": f"خطأ: أداة غير معروفة '{tool_name}'"
                    })

    def _format_results(self, result: dict) -> str:
        if not result["success"]:
            return f"فشل البحث: {result.get('error', 'خطأ غير معروف')}"

        if not result["results"]:
            return "لم يتم العثور على نتائج"

        formatted = []
        for r in result["results"]:
            formatted.append(f"- {r['title']}: {r['snippet'][:200]}...")

        return "\n".join(formatted)

    def _synthesize_report(self, topic: str) -> str:
        """توليد التقرير النهائي من النتائج"""
        findings = self.memory.get_all_findings()
        sources = self.memory.get_sources()

        sources_text = "\n".join([
            f"[{s['id']}] {s['title']}: {s['url']}"
            for s in sources
        ])

        prompt = SYNTHESIS_PROMPT.format(
            topic=topic,
            findings=findings,
            sources=sources_text,
            max_length=self.config.REPORT_MAX_LENGTH
        )

        response = self.llm.invoke([{"role": "user", "content": prompt}])

        return response.content

قرارات التصميم الرئيسية

القرارالمبرر
نمط ReActاستدلال شفاف، قابل للتحكم
ذاكرة منفصلةنتائج مستمرة عبر التكرارات
تجريد الأدواتسهولة إضافة مصادر بحث جديدة
خطوة التجميعجودة أفضل من الإخراج المتدفق

التالي: إضافة الاختبار والتحقق لضمان نتائج موثوقة. :::

مراجعة سريعة: كيف تجد هذا الدرس؟

اختبار

الوحدة 5: بناء وكيل بحث

خذ الاختبار
نشرة أسبوعية مجانية

ابقَ على مسار النيرد

بريد واحد أسبوعياً — دورات، مقالات معمّقة، أدوات، وتجارب ذكاء اصطناعي.

بدون إزعاج. إلغاء الاشتراك في أي وقت.