فاز 19 · درس 24

جریان کنترل برنامه‌ریزی و اجرا در ایجنت‌ها

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
نمای یک دفترچه راهنمای کاغذی با چک‌لیست مراحل که برخی خط خورده‌اند و مسیر جدیدی در کنارشان با یک خودکار رسم شده است.
فهرست مطالب
  1. اهداف یادگیری
  2. برنامه‌ریزی و اجرا، نه زنجیره‌ی تفکر
  3. ساختار مرحله
  4. ساختار برنامه‌ریز
  5. اجراکننده
  6. تفاوت برنامه در هر بازنگری
  7. دو بودجه، هر دو قطعی
  8. برنامه‌ریز قطعی در این درس
  9. ساختار خروجی
  10. راهنمای خواندن کد
  11. گام‌های بعدی

در ساخت سیستم‌های مبتنی بر مدل‌های زبانی، تکیه بر زنجیره‌ی تفکر پیوسته برای کارهای چندمرحله‌ای اغلب ناپایدار است؛ چون خطا در یک فراخوانی ابزار کل زنجیره را مختل می‌کند. معماری برنامه‌ریزی و اجرا (Plan-Execute) این چالش را با جداسازی تصمیم‌گیری از اجرا حل می‌کند: ابتدا یک برنامه‌ی ساختاریافته تولید می‌شود و سپس هر گام به‌طور قطعی و نظارت‌شده پیش می‌رود.

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

برنامه‌ای که در برابر شکست تاب نیاورد، فقط یک اسکریپت است. اسکریپتی که بتواند دوباره برنامه‌ریزی کند، یک ایجنت (Agent) است. اول بازبرنامه‌ریز (replanner) را بسازید.

اهداف یادگیری

  • نمایش برنامه به‌صورت فهرستی مرتب از مراحل تایپ‌شده (typed steps)، تا اجراکننده بتواند درباره‌ی روند پیشرفت و نتیجه استدلال کند.
  • اجرای ترتیبی مراحل، همراه با دست‌به‌دست‌کردن (Handoff) مهارشده‌ی شکست به برنامه‌ریز.
  • بازبرنامه‌ریزی از مکان‌نمای فعلی با قرار دادن خطای پیشین (Prior error) در کانتکست، تا برنامه‌ی بعدی آگاهانه چیده شود.
  • صدور تفاوت برنامه (plan diff) در هر بازنگری، تا ابزار ردگیری محاسبات (trace) یا رابط کاربری بتواند دلیل تغییر برنامه را نشان دهد.
  • اعمال دو سقف بودجه‌ی قطعی: سقف سخت‌گیرانه‌ی تعداد مراحل و سقف سخت‌گیرانه‌ی دفعات بازبرنامه‌ریزی.
برنامه‌ریزی / اجرا / بازبرنامه‌ریزیخطا کنترل را به برنامه‌ریز بازمی‌گرداند
PLAN / EXECUTE / REPLANThe plan is an ordered list of typed steps the executor can walk, not prose the loop has to parse. When step 3 fails, the executor does not improvise: it hands the cursor and the error back to the planner, which returns a fresh tail from that position. The revision arrives as a diff a tracer can display, and two hard ceilings, one on steps and one on replans, keep the loop from planning forever.برنامه داده است · مجری آن را پیمایش می‌کندplannerreplan(cursor, err)1 خواندن فایل2 اجرای تست‌ها3 اعمال تکه4 تست مجدد5 گزارش✗ خطای import3' اصلاح import4' اجرای مجدد تستdiff +2 −2بودجه‌ها: گام‌ها 7/8 · بازبرنامه‌ریزی 1/2 · توقف با عبور از هر سقف
برنامه یک فهرست مرتب از گام‌های نوع‌دار است که مجری می‌تواند آن را طی کند، نه یک متن عادی که حلقه ناچار به تحلیلش باشد. وقتی گام 3 شکست می‌خورد، مجری بداهه‌پردازی نمی‌کند: مکان‌نما و خطا را به برنامه‌ریز بازمی‌گرداند و برنامه‌ریز دنباله‌ای تازه از همان نقطه ارائه می‌دهد. این بازنگری به‌صورت یک diff بازمی‌گردد که ابزار ردگیری می‌تواند آن را نمایش دهد، و دو سقف سخت‌گیرانه، یکی روی تعداد گام‌ها و دیگری روی بازبرنامه‌ریزی‌ها، مانع از برنامه‌ریزی بی‌پایان حلقه می‌شوند.

برنامه‌ریزی و اجرا، نه زنجیره‌ی تفکر

ایجنت مبتنی بر زنجیره‌ی تفکر (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 برمی‌گردانند.

موفقیتn+1 < len(plan)n+1 == len(plan)شکستبرنامه جدید، replans_used <max_replansreplans_used >= max_replansEXECNEXTDONEREPLANFAILED

تفاوت برنامه در هر بازنگری

وقتی برنامه‌ریز پس از شکست برنامه‌ی جدیدی برمی‌گرداند، اجراکننده یک رویداد 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)، می‌توان دو فراخوانی ابزار را هم‌زمان از طریق توزیع‌کننده اجرا کرد.

هر دو قابلیت پیچیدگی واقعی سیستم را بالا می‌برند. اضافه کردن هر دوی آن‌ها زمانی بسیار ساده‌تر می‌شود که اجراکننده‌ی خطی کاملاً تثبیت شده باشد؛ و این دقیقاً همان کاری است که در این درس انجام می‌دهیم.

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسوظیفه برنامه‌ریز (planner) در یک ایجنت plan-execute چیست؟
  2. پیش از درسچرا با اینکه مجری expected_outcome را بررسی نمی‌کند، این فیلد بخشی از ساختار Step است؟
  3. حین درسهنگامی که یک گام با شکست مواجه می‌شود و بازبرنامه‌ریز برنامه جدیدی ارائه می‌دهد، مجری چه چیزی را روی جریان رویدادها ارسال می‌کند؟
  4. حین درسچرا max_steps و max_replans به‌عنوان دو بودجه جداگانه در نظر گرفته شده‌اند؟
  5. حین درسهنگامی که برنامه‌ریز در اولین فراخوانی یک فهرست خالی برمی‌گرداند، چه دلیلی (reason) بازگردانده می‌شود؟
  6. پس از درسچه مواردی در PlanDiff.revised قرار دارند؟
  7. پس از درسهنگامی که مجری سقف max_replans را تمام می‌کند، SessionResult.reason حاصل چیست؟

سؤالات متداول

تفاوت اصلی الگوی برنامه‌ریزی و اجرا با زنجیره‌ی تفکر چیست؟

در زنجیره‌ی تفکر، مدل توکن‌ها را پشت سر هم تولید می‌کند و پایان فراخوانی ابزار باید حدس زده شود؛ اما در الگوی برنامه‌ریزی و اجرا، یک برنامه‌ی داده‌ای ساختاریافته تولید می‌شود و بستر برنامه هر گام را جداگانه و قطعی اجرا می‌کند.

چرا در این معماری از دو بودجه max_steps و max_replans استفاده می‌شود؟

مقدار max_steps سقف کل مراحل اجراشده در نشست را مهار می‌کند، در حالی که max_replans جلوی حلقه‌ی بی‌پایان برنامه‌ریزی مجددِ برنامه‌های معیوب را می‌گیرد تا شکست‌ها سریع‌تر و با دلیل شفاف مشخص شوند.

رویداد تفاوت برنامه (plan diff) چه کاربردی دارد؟

این رویداد مراحل حذف‌شده و اضافه‌شده را در هر بازنگری مشخص می‌کند تا ابزارهای ردگیری (Tracer) یا رابط‌های کاربری بتوانند دلیل و جزئیات تغییر برنامه را بدون بازنویسی پنهان به تصویر بکشند.

ما رو توی گوگل بیشتر ببین

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