در ساخت سیستمهای مبتنی بر مدلهای زبانی، تکیه بر زنجیرهی تفکر پیوسته برای کارهای چندمرحلهای اغلب ناپایدار است؛ چون خطا در یک فراخوانی ابزار کل زنجیره را مختل میکند. معماری برنامهریزی و اجرا (Plan-Execute) این چالش را با جداسازی تصمیمگیری از اجرا حل میکند: ابتدا یک برنامهی ساختاریافته تولید میشود و سپس هر گام بهطور قطعی و نظارتشده پیش میرود.
ارزش کلیدی این روش در سازوکار بازبرنامهریزی (Replanning) هنگام بروز خطا است. بهجای توقف ناگهانی اسکریپت یا تکرارهای بیپایان و بیهدف، سیستم خطا را در کانتکست قرار میدهد، برنامهی باقیمانده را بهروزرسانی میکند و با اعمال بودجههای قطعی از هدررفت منابع جلوگیری مینماید.
برنامهای که در برابر شکست تاب نیاورد، فقط یک اسکریپت است. اسکریپتی که بتواند دوباره برنامهریزی کند، یک ایجنت (Agent) است. اول بازبرنامهریز (replanner) را بسازید.
اهداف یادگیری
- نمایش برنامه بهصورت فهرستی مرتب از مراحل تایپشده (typed steps)، تا اجراکننده بتواند دربارهی روند پیشرفت و نتیجه استدلال کند.
- اجرای ترتیبی مراحل، همراه با دستبهدستکردن (Handoff) مهارشدهی شکست به برنامهریز.
- بازبرنامهریزی از مکاننمای فعلی با قرار دادن خطای پیشین (Prior error) در کانتکست، تا برنامهی بعدی آگاهانه چیده شود.
- صدور تفاوت برنامه (plan diff) در هر بازنگری، تا ابزار ردگیری محاسبات (trace) یا رابط کاربری بتواند دلیل تغییر برنامه را نشان دهد.
- اعمال دو سقف بودجهی قطعی: سقف سختگیرانهی تعداد مراحل و سقف سختگیرانهی دفعات بازبرنامهریزی.
برنامهریزی و اجرا، نه زنجیرهی تفکر
ایجنت مبتنی بر زنجیرهی تفکر (chain-of-thought) توکنها (token) را تولید میکند و به عهدهی حلقه میگذارد تا حدس بزند فراخوانی ابزار (tool call) کجا پایان مییابد. در مقابل، ایجنت برنامهریزی و اجرا (plan-and-execute) ابتدا برنامهای ساختاریافته تولید میکند، سپس هر مرحله را بهصورت قطعی پیش میبرد. برنامه در واقع دادهای است که بستر آزمایشی (harness) میتواند ساختار درونیاش را بررسی کند. اجرا هم یعنی بستر آزمایشی همان دادهها را از طریق یک توزیعکننده (dispatcher) پردازش کند.
دو بخش اصلی داریم: برنامهریزی که برنامه را تولید میکند، و اجراکنندهای که آن را اجرا میکند. بخش هیجانانگیز ماجرا زمانی رخ میدهد که اجراکننده به شکست برمیخورد. سه راه پیش رو داریم:
1. Abort (return failed, surface the error)
2. Skip (mark step failed, continue with the rest)
3. Replan (hand the error to the planner, get a new plan from the cursor)
بازبرنامهریزی دقیقاً همان چیزی است که اسکریپت را به ایجنت تبدیل میکند.
ساختار مرحله
Step
id : int (monotonic within a plan revision)
tool_name : str
args : dict
expected_outcome: str (planner's stated success condition)
result : Any | None
error : str | None
فیلد expected_outcome جملهی کوتاهی است که برنامهریز در کنار هر مرحله صادر میکند. اجراکننده رعایت این مورد را الزامی نمیکند؛ این فیلد دو کاربرد دارد: بازبرنامهریز هنگام بازنگری برنامه آن را میخواند؛ جریان رویدادها هم آن را منتشر میکند تا ابزار ردگیری محاسبات بتواند نشان دهد که «قرار بود این مرحله کار X را انجام دهد».
ساختار برنامهریز
def planner(goal: str, history: list[Step], last_error: str | None) -> list[Step]:
...
یک تابع خالص (Pure Function). آرگومان goal همان هدف کاربر است. مقدار history مراحلی را نگه میدارد که قبلاً اجرا شدهاند (همراه با نتایج و خطاهای ثبتشده). مقدار last_error در اولین فراخوانی برابر با None است و در فراخوانیهای بعدی آخرین پیام شکست را در بر دارد. برنامهریز، برنامهی بعدی را با شروع از مکاننمای فعلی برمیگرداند.
برنامهریز هیچ اطلاعی از اجراکننده ندارد؛ از تلاشهای مجدد (retries) بیخبر است و مهلتهای زمانی (timeouts) را هم نمیشناسد. برنامهریز فقط یک برنامه میسازد؛ کارش همین است.
اجراکننده
اجراکننده یک ماشین وضعیت (State Machine) کوچک است. هر مرحله از طریق توزیعکننده اجرا میشود. خروجی کار یکی از این سه حالت است: موفقیت، شکستِ قابل بازبرنامهریزی، یا شکست مهلک. شکستهای قابل بازبرنامهریزی، کار را دوباره به برنامهریز تحویل میدهند. شکستهای مهلک (فراتر رفتن از بودجه، رسیدن به سقف بازبرنامهریزی) نتیجهی نشست (session) را برابر با FAILED برمیگردانند.
تفاوت برنامه در هر بازنگری
وقتی برنامهریز پس از شکست برنامهی جدیدی برمیگرداند، اجراکننده یک رویداد plan.diff با سه فیلد صادر میکند.
removed: list of step ids that were in the old plan and are not in the new
added : list of step ids in the new plan that were not in the old
revised: list of step ids whose tool_name or args changed
ابزار ردگیری محاسبات یا رابط کاربری میتواند مراحل حذفشده را خطخورده و مراحل جدید را برجسته (highlight) نشان دهد. هدف اصلی، فرمتِ تفاوت (diff) نیست؛ مهم این است که بازنگری یک رویداد شفاف و آشکار است، نه بازنویسیای پنهان و بیصدا.
دو بودجه، هر دو قطعی
مقدار max_steps سقف کل مراحل اجراشده در سرتاسر نشست را—با احتساب بازبرنامهریزیها—مشخص میکند؛ مقدار پیشفرض آن 12 است. تلاشهای ناموفق هم شمرده میشوند. برنامهای 5مرحلهای که در گام آخر شکست بخورد، 5 واحد اجرا مصرف کرده است. اگر هر بازبرنامهریزی همان مرحله را دوباره تکرار کند و 3 مرحلهی جدید بیفزاید، و آخرین مرحلهی اولین بازنگری هم باز با شکست روبهرو شود، تعداد کل اجراها به 9 میرسد؛ بازنگری دوم هم 4 اجرای دیگر میخواهد که مجموعاً میشود 13 تا. اجراکننده دوازدهمین مرحله را اجرا میکند، از اجرای سیزدهمی سر باز میزند و وضعیت FAILED را با دلیل step_budget برمیگرداند.
مقدار max_replans سقف دفعات فراخوانی برنامهریز پس از ارائهی برنامهی اولیه را تعیین میکند؛ مقدار پیشفرض 5 است. این محدودیت اهمیت بیشتری دارد. برنامهریزی که همان برنامهی معیوب را 5 بار پیاپی برگرداند، در صورت نبود این سقف آنقدر تکرار میشود تا بالاخره به بودجهی مراحل برخورد کند. گذاشتن سقف برای بازبرنامهریزی باعث میشود شکست سریعتر رخ دهد و علت آن بسیار شفافتر باشد.
برنامهریز قطعی در این درس
در این درس مدلی را فراخوانی نمیکنیم. این درس همراه با یک برنامهریز قطعی ارائه میشود که بر اساس last_error برنامه را انتخاب میکند.
last_error is None -> emit a four-step plan
last_error matches X -> emit a three-step plan that routes around X
last_error matches Y -> emit a two-step plan that gives up gracefully
otherwise -> return [] (signals nothing to replan)
همین ساختار کافی است تا رفتار اجراکننده را در تمام مسیرهای گذار بیازمایید: موفقیت، یک بار بازبرنامهریزی، دو بار بازبرنامهریزی، تمام شدن فرصتهای بازبرنامهریزی، و تمام شدن بودجهی مراحل.
ساختار خروجی
SessionResult
status : "completed" | "failed"
reason : str ("goal_met" | "step_budget" | "replan_budget" | "no_plan")
history : list[Step]
revisions : list[PlanDiff]
events : list[Event]
حلقهی بستر آزمایشی درس بیستم میتواند مستقیماً این ساختار را بخواند. توزیعکنندهی درس بیستوسوم هر مرحله را اجرا میکند. رجیستری درس بیستویکم آرگومانهای هر مرحله را اعتبارسنجی میکند. بستر انتقال (transport) درس بیستودوم هم میتواند تمام این جریان کاری (flow) را روی JSON-RPC در اختیار کلاینت مدل بگذارد.
راهنمای خواندن کد
فایل code/main.py موجودیتهای PlanExecuteAgent، Step، PlanDiff، SessionResult و برنامهریز قطعی را تعریف میکند. اجراکننده متد تکیِ run(goal) است که یک SessionResult برمیگرداند. تفاوت برنامه نیز با مقایسهی شناسهی مراحل و تاپلهای (tool_name, args) محاسبه میشود.
فایل code/tests/test_agent.py حالتهای مختلف را پوشش میدهد: موفقیت خطی، شکست میانهی برنامه همراه با یک بار بازبرنامهریزی، تمام شدن فرصت بازبرنامهریزی با خروجی failed:replan_budget، تمام شدن بودجهی مراحل، و قالب رویداد plan-diff.
گامهای بعدی
وقتی این منطق را به مدلی واقعی متصل کنید، به دو قابلیت تکمیلی نیاز خواهید داشت. نخست، کش کردن بخشی از برنامه (partial-plan caching): وقتی 3 مرحله از برنامهای 6مرحلهای با موفقیت اجرا میشود و سپس به خطا میخورد، دلیلی ندارد آن سه مرحلهی اول دوباره اجرا شوند. اجراکننده تاریخچه را نگه میدارد؛ فقط کافی است برنامهریز آن را بخواند. دوم، branchهای موازی: اجراکنندهی فعلی کاملاً ترتیبی کار میکند. اگر برنامهریز یک branch مستقل خروجی دهد (مثلاً gather_step به جای next_step)، میتوان دو فراخوانی ابزار را همزمان از طریق توزیعکننده اجرا کرد.
هر دو قابلیت پیچیدگی واقعی سیستم را بالا میبرند. اضافه کردن هر دوی آنها زمانی بسیار سادهتر میشود که اجراکنندهی خطی کاملاً تثبیت شده باشد؛ و این دقیقاً همان کاری است که در این درس انجام میدهیم.
