پرش به محتوای اصلی
پرش به محتوای مقاله

۱۷۰ خط کد پایتون در برابر لنگ‌چین؛ چرا پیاده‌سازی دستی حلقه‌های ReAct برنده است

·۶ شهریور ۱۴۰۵۹ دقیقه مطالعه
راهنما
حلقه عامل ReAct از صفر: پیاده‌سازی دستی در مقابل LangChain
حلقه عامل ReAct از صفر: پیاده‌سازی دستی در مقابل LangChain
اشتراک‌گذاری
واقعاً چه چیز جدید است؟

تأکید بر برتری کدنویسی دستی (۱۷۰ خط) بر انتزاع‌های آماده در ساخت عامل‌های ReAct؛ این خبر سیگنالی است برای بازگشت به شفافیت کد در مقابل «جادوی» فریم‌ورک‌ها.

اگر امروز برای ساخت یک عامل هوشمند به فریم‌ورک‌های پیچیده تکیه می‌کنید، احتمالاً بخشی از کنترل سیستم را فدای سرعت توسعه کرده‌اید. حقیقت این است که تنها ۱۷۰ خط کد پایتون می‌تواند عملکردی به مراتب دقیق‌تر و قابل‌ پیش‌بینی‌تر از ابزارهای سطح بالا ارائه دهد. در حالی که ابزارهایی مانند LangChain می‌توانند این حجم کد را به ۱۰ خط کاهش دهند، اما اغلب مکانیسم‌های حیاتی تجزیه (Parsing) و بازیابی خطا را پنهان می‌کنند؛ مکانیسم‌هایی که تعیین می‌کنند یک عامل در محیط عملیاتی موفق شود یا شکست بخورد.

ساخت یک عامل خودمختار دیگر تنها به مهندسی پرامپت محدود نمی‌شود، بلکه تماماً دربارهٔ «حلقه» (Loop) است. اکثر عامل‌های مدرن از الگوی ری‌اکت (ReAct) — شبیه به یک کارآگاه که ابتدا فکر می‌کند، سپس شواهدی را جمع می‌کند و بر اساس آن‌ها نتیجه می‌گیرد — استفاده می‌کنند. این چرخه از «تفکر، اقدام و مشاهده»، یک چت‌بات ساده را به عاملی تبدیل می‌کند که قادر به اجرای وظایف واقعی است. همان‌طور که در تحلیل قبلی ما درباره‌ی اثرات Thermal Throttling (گلوگاه حرارتی) بر پایداری این حلقه‌ها در دستگاه‌های Edge اشاره کردیم، اکنون ثبات پیاده‌سازی نرم‌افزاری به گلوگاه اصلی تبدیل شده است.

کالبدشکافی یک حلقهٔ دستی

به نقل از راهنمای فنی منتشر شده در ۲۸ اوت ۲۰۲۶ در وب‌سایت dev.to، یک عامل ReAct کاربردی از سه بخش اصلی تشکیل شده است: ثبت ابزارها (Tool Registry)، یک پرامپت سیستمی سخت‌گیرانه و حلقهٔ اجرا.

بخش ثبت ابزارها سیستمی مینیمال است که در آن هر ابزار دارای نام، توصیف برای مدل و یک تابع قابل‌فراخوانی است. در پیاده‌سازی خالص پایتون، این مورد معمولاً از طریق یک @dataclass به نام Tool مدیریت می‌شود که شامل فیلدهای name (نام)، description (توصیف)، func (تابع) و یک دیکشنری parameters برای پارامترها است. متد run در این کلاس تضمین می‌کند که نتایج همیشه به صورت رشته (String) بازگردانده شوند و اگر نتیجه از پیش رشته نباشد، از json.dumps برای تبدیل آن استفاده می‌کند.

برای مثال، یک ابزار هواشناسی می‌تواند تابعی ساده باشد که با استفاده از کتابخانه httpx داده‌ها را از Open-Meteo می‌گیرد بدون اینکه نیازی به API Key باشد. این ابزار خاص ابتدا با فراخوانی API ژئوکدینگ (https://geocoding-api.open-meteo.com/v1/search) و ارسال پارامترهایی برای نام شهر، تعداد نتایج، زبان و فرمت، طول و عرض جغرافیایی شهر را می‌یابد. سپس API پیش‌بینی هوا (https://api.open-meteo.com/v1/forecast) را با پارامتر current_weather=true فراخوانی می‌کند تا دمای لحظه‌ای و سرعت باد را استخراج کند. خروجی نهایی یک رشته فرمت‌شده شامل نام شهر، کشور، دما به سلسیوس و سرعت باد بر حسب کیلومتر بر ساعت است.

حلقه عامل ReAct از پایه: پیاده‌سازی دستی در مقابل LangChain

پرامپت سیستمی ReAct

هوش این سیستم در پرامپت سیستمی (System Prompt) نهفته است. این پرامپت، مدل زبانی بزرگ (LLM) — مثل کتابخانه‌داری که میلیاردها صفحه را خوانده و حالا با همان لحن جواب می‌دهد — را مجبور می‌کند از یک فرمت صلب و سخت‌گیرانه پیروی کند: تفکر $\rightarrow$ اقدام $\rightarrow$ ورودی اقدام $\rightarrow$ مشاهده.

در این ساختار، مدل صراحتاً دستور می‌گیرد که «همیشه قبل از اقدام فکر کن»، «در هر مرحله تنها یک ابزار را فراخوانی کن» و «اگر ابزاری خطا داد، روش دیگری را امتحان کن». همچنین تصریح شده است که وقتی عامل اطلاعات کافی را به دست آورد، باید یک «پاسخ نهایی» (Final Answer) ارائه دهد. این نظم به توسعه‌دهنده اجازه می‌دهد خروجی مدل را با استفاده از متدهای استاندارد رشته در پایتون، خط‌به‌خط تجزیه کند و مطمئن شود مدل پیش از تلاش برای فراخوانی ابزار، مرحلهٔ استدلال (Reasoning) را نادیده نگرفته است.

چرخهٔ اجرا و مدیریت خطا

هستهٔ اصلی عامل در یک چرخه تکرارشونده عمل می‌کند که معمولاً برای جلوگیری از حلقه‌های بی‌نهایت، با سقف max_steps (مثلاً ۸ مرحله) محدود شده است:

  • پرامپت‌نویسی: سیستم توصیف ابزارها را در پرامپت سیستمی جایگذاری کرده و پرسش کاربر را ارسال می‌کند. در اینجا از یک REACT_SYSTEM_PROMPT استفاده می‌شود که لیست تمام ابزارهای موجود و توصیفات دقیق آن‌ها را در بر دارد.
  • تجزیه (Parsing): سیستم بررسی می‌کند آیا «پاسخ نهایی» تولید شده است یا خیر. در غیر این صورت، نام ابزار (Action) و ورودی JSON آن (Action Input) را با استفاده از یک متد کمکی مانند _parse_action استخراج می‌کند که متن را بر اساس خطوط جدا کرده و فضاهای خالی (Whitespace) را حذف می‌کند.
  • اجرا: ابزار مربوطه اجرا می‌شود. اگر ورودی JSON نامعتبر باشد، سیستم طوری طراحی شده که از طریق یک بلوک json.JSONDecodeError به ورودی رشته‌ای ساده بازگردد. نتیجه به عنوان یک «مشاهده» (Observation) ثبت می‌شود.
  • بازخورد: نتیجهٔ مشاهده به تاریخچهٔ گفتگو به عنوان یک پیام کاربر (User Message) اضافه شده و حلقه تکرار می‌شود.

در پیاده‌سازی دستی، اگر مدل فرمت را رعایت نکند یا ابزاری را فراخوانی کند که وجود ندارد، توسعه‌دهنده می‌تواند یک «تلنگر» (Nudge) ارسال کند. مثلاً پیامی مبنی بر «فرمت نامعتبر است یا ابزار ناشناخته است. دقیقاً از این فرمت استفاده کن: Thought: ... Action: Action Input: » را به مدل برگرداند تا پیش از تلاش بعدی، خطای خود را اصلاح کند.

مثال عملی از زمان اجرا

در پاسخ به پرسشی مثل «هوای استانبول چطور است و حاصل ۱۵ ضربدر ۷ چند می‌شود؟»، حلقهٔ دستی استدلال را در لحظه نمایش می‌دهد:

۱. گام اول: مدل استدلال می‌کند که به دو قطعه اطلاعات نیاز دارد. ابزار get_weather را با ورودی {"city": "Istanbul"} فراخوانی می‌کند. نتیجه این است: «هوای استانبول، ترکیه: ۲۲ درجه سلسیوس، باد ۱۴ کیلومتر بر ساعت».
۲. گام دوم: مدل سپس ابزار calculator را با ورودی {"expression": "15 * 7"} اجرا کرده و عدد «۱۰۵» را دریافت می‌کند.
۳. گام نهایی: مدل این مشاهدات را ترکیب کرده و پاسخ نهایی را می‌سازد: «هوای استانبول ۲۲ درجه با باد ۱۴ کیلومتر بر ساعت است و حاصل ۱۵ ضربدر ۷ برابر با ۱۰۵ می‌شود».

انعطاف‌پذیری در حافظه و بک‌اِند

حافظه اغلب نادیده گرفته می‌شود، اما در رویکرد دستی می‌توان از یک ConversationBufferMemory ساده برای نگه داشتن پنجره‌ای از پیام‌های اخیر استفاده کرد. این بخش به صورت یک dataclass با محدودیت max_messages (مثلاً ۲۰ پیام) پیاده‌سازی می‌شود که در آن لیست پیام‌ها برای حفظ جدیدترین ورودی‌ها برش (Slice) می‌خورد.

برای نیازهای بلندمدت، توسعه‌دهندگان می‌توانند ChromaDB را برای ایجاد یک سیستم VectorMemory ادغام کنند. این سیستم از یک PersistentClient با یک مسیر ذخیره‌سازی (persist_dir) مشخص برای ذخیره و پرس‌وجوی معنایی اسناد استفاده می‌کند. این ساختار به عامل اجازه می‌دهد با استفاده از متد query که برترین نتایج (n_results که پیش‌فرض آن ۵ است) را برمی‌گرداند، زمینه‌های مرتبط را از تعاملات گذشته بازیابی کند.

یکی از بزرگ‌ترین مزایای این روش، سهولت در تعویض مدل است. با استفاده از Wrapperهای LangChain (تنها برای بخش LLM و نه برای انتزاع‌های عامل)، تنها با تغییر یک رشته در متد _call_llm می‌توان بین مدل‌های مختلف جابه‌جا شد:

  • OpenAI: استفاده از ChatOpenAI با مدل gpt-4o-mini و کلید API از تنظیمات.
  • Anthropic: استفاده از ChatAnthropic با مدل claude-3-5-sonnet-20241022.
  • Ollama: اجرای محلی llama3.1:8b روی آدرس http://localhost:11434.

موازنه: دستی در برابر فریم‌ورک

مقایسه پیاده‌سازی دستی با create_tool_calling_agent و AgentExecutor در LangChain تضاد شدیدی در سطح دید (Visibility) و کنترل نشان می‌دهد:

  • حجم کد: روش دستی حدود ۱۷۰ خط است؛ لنگ‌چین حدود ۱۰ خط.
  • تجزیه: در روش دستی دید کامل به هر خط وجود دارد؛ لنگ‌چین این فرآیند را پشت Executor پنهان می‌کند.
  • بازیابی خطا: روش دستی اجازه تلنگرهای سفارشی در صورت شکست تجزیه را می‌دهد؛ لنگ‌چین از منطق تکرار (Retry) پیش‌فرض استفاده می‌کند.
  • حافظه: روش دستی از ConversationBufferMemory یا VectorMemory سفارشی استفاده می‌کند؛ لنگ‌چین از کلاس‌های حافظه داخلی خود بهره می‌برد.
  • پرامپت‌نویسی: در روش دستی مالکیت ۱۰۰٪ پرامپت سیستمی با شماست؛ لنگ‌چین آن را به صورت خودکار فرمت می‌کند.
  • دیباگ: در روش دستی هر گام چاپ می‌شود؛ لنگ‌چین به verbose=True متکی است.
  • منحنی یادگیری: روش دستی نیازمند خواندن کد برای درک هر خط است؛ لنگ‌چین نیازمند خواندن مستندات و اعتماد به انتزاع‌هاست.

زمان رها کردن انتزاع‌ها

برای نمونه‌سازی سریع، لنگ‌چین به دلیل سرعت و اکوسیستم گسترده از Loaderها و Retrieverها همچنان برنده است. اما برای سیستم‌های تولیدی که در آن هر تکرار حلقه باید قابل دیباگ باشد، رویکرد دستی ضروری است. در محیط عملیاتی، «جادوی» فریم‌ورک‌ها یک ریسک است. وقتی عاملی در حلقه بی‌نهایت می‌افتد یا در فراخوانی ابزار شکست می‌خورد، دیدن دقیق تبادل پیام‌ها تنها راه تشخیص خطا است. حلقه دستی لایه انتزاع را حذف کرده و هر رفتار را در کد مرئی می‌کند. در واقع برای حل مشکل توقف ناگهانی عامل‌ها در محیط‌های پیچیده، استفاده از نقاط بازرسی پایدار در LangGraph.js راهکاری مدرن است که مکمل کنترل دستی در مقیاس بزرگ است.

این چرخش به سمت کنترل دستی، بازتابی از یک روند گسترده‌تر در مهندسی هوش مصنوعی است: حرکت از فریم‌ورک‌های «جعبه سیاه» به سمت خط‌لوله‌های شفاف و ماژولار. یک کیت توسعه کامل در این رویکرد معمولاً شامل فایل‌های زیر است:

  • agent.py: منطق اصلی حلقه ReAct.
  • tools.py: ثبت ابزارهایی مانند هواشناسی، ماشین‌حساب و جستجوی وب.
  • memory.py: پیاده‌سازی بافرهای گفتگو و حافظه برداری ChromaDB.
  • rag_pipeline.py: خط‌لوله RAG با استفاده از LlamaIndex.
  • fine_tune.py: اسکریپت‌های Fine-tuning محلی برای LLM با استفاده از Unsloth.
  • ollama_setup.py: کمکی برای استقرار مدل‌های محلی.
  • notebooks/: نوت‌بوک‌های Jupyter گام‌به‌گام برای هر جزء.
  • guide/: مستندات Markdown درباره معماری، RAG و استقرار در محیط تولید.

برای تسلط واقعی بر جریان‌های کاری عاملی (Agentic Workflows)، توسعه‌دهندگان باید با پیاده‌سازی حلقه ReAct از صفر شروع کنند. این مسیر می‌تواند به ایجاد سیستم‌های پیشرفته‌تری منجر شود، مشابه آنچه در رویکرد Prime Agent برای ساخت عامل‌های خودبهبودبخش مشاهده می‌کنیم که از محیط‌های پایتون دائمی بهره می‌برد. زمانی که مکانیسم‌های تجزیه و مشاهده درک شوند، انتزاع‌های ارائه شده توسط فریم‌ورک‌ها به ابزارهایی تبدیل می‌شوند که آگاهانه استفاده می‌شوند، نه وابستگی‌هایی که کورکورانه به آن‌ها اعتماد شود.

گام بعدی شما

  • پیاده‌سازی یک حلقهٔ ReAct ساده بدون هیچ فریم‌ورکی برای درک مکانیسم Parsing.
  • جایگزینی AgentExecutor لنگ‌چین با یک حلقهٔ دستی در پروژه‌هایی که نرخ خطای بالایی در فراخوانی ابزارها دارند.
  • تست مدل‌های کوچک‌تر (مثل Llama 3.1 8B) در حلقهٔ دستی برای سنجش توانایی استدلال آن‌ها در مقابل مدل‌های بزرگ.

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

چرا این موضوع مهم است؟

این رویکرد با حذف لایه‌های پنهان، قابلیت اطمینان (Reliability) عامل‌های هوش مصنوعی را در مقیاس صنعتی افزایش می‌دهد. توسعه‌دهندگانی که کنترل حلقه را در دست بگیرند، می‌توانند نرخ توهم در فراخوانی ابزارها را به شدت کاهش دهند.

تأثیر برای ایران

برای توسعه‌دهندگان ایرانی که با محدودیت‌های API و هزینه‌های استنتاج روبرو هستند، پیاده‌سازی دستی امکان بهینه‌سازی دقیق‌تر توکن‌ها و استفاده از مدل‌های محلی (Ollama) را بدون وابستگی به فریم‌ورک‌های سنگین فراهم می‌کند.

·نگاه ما
تحریریه دات‌هوش

وابستگی شدید به فریم‌ورک‌های انتزاعی مانند LangChain در مراحل اولیه رشد اکوسیستم AI، اکنون به یک مانع برای پایداری سیستم‌های تولیدی تبدیل شده است. بازگشت به پیاده‌سازی‌های مینیمال و دستی نشان می‌دهد که در مهندسی عامل‌ها، شفافیت در مدیریت خطا (Error Handling) بسیار ارزشمندتر از کاهش تعداد خطوط کد است. این رویکرد در واقع گذاری از «برنامه‌نویسی با ابزار» به «مهندسی جریان استدلال» است.

منابع

این گزارش با خط‌لولهٔ خودکار دات‌هوش از منابع معتبر جهانی تدوین و زیر نظر تحریریه منتشر شده است. روش کار ما

گفتگو

پنج‌شنبه‌های هوش‌محور

بسته‌ی هفتگی دات‌هوش

۵ خبر، ۲ ابزار، ۱ پرامپت در هر شماره. به‌زودی راه‌اندازی می‌شود — هر پنج‌شنبه صبح.

خبر کلیدی
ابزار کاربردی
پرامپت حرفه‌ای
تحلیل پژوهش
به‌زودی
زاویه‌ی ایرانی
به‌زودی
تمرین این هفته
به‌زودی

راهنماهای دات‌هوش

راهنماهای کاربردیِ دات‌هوش برای کار با هوش مصنوعی — از همین‌جا شروع کنید:

دات‌هوش

راهنمای فارسی هوش مصنوعی — با نگاه به ایران

اخبار روزانه، معرفی ابزارها و مدل‌ها، و آموزشِ کار با هوش مصنوعی؛ همیشه با این پرسش که از ایران چه چیزی کار می‌کند و چه چیزی نه.