فاز 13 · درس 3

فراخوانی موازی و استریمینگ ابزارها در مدل‌های زبانی

نوع
ساختنی
زمان
حدود 75 دقیقه
زبان‌ها
Python (stdlib, thread pool + streaming harness)
پیش‌نیاز
فاز 13 · درس 02 (بررسی عمیق فراخوانی تابع)
چند خط لوله‌ی شیشه‌ای موازی با مایعات رنگی درخشان که در یک مخزن مشترک متصل به برچسب‌های شناسایی ادغام می‌شوند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. فعال‌سازی حالت موازی
  5. تطبیق شناسه‌ها
  6. اجرای هم‌زمان فراخوانی‌ها
  7. استریمینگ فراخوانی‌های ابزار
  8. ساختار JSON ناقص و تله‌ی پارس زودهنگام
  9. اتمام نامرتب و بدون ترتیب
  10. بنچمارک: متوالی در برابر موازی
  11. زمان واقعی در توزیع استریمینگ
  12. استفاده از آموخته‌ها
  13. اجرای عملی
  14. تمرین‌ها
  15. اصطلاحات کلیدی
  16. برای مطالعه‌ی بیشتر

وقتی یک ایجنت هوش مصنوعی نیاز دارد اطلاعات چند شهر یا منبع مختلف را بررسی کند، اجرای پیاپی و متوالی این درخواست‌ها تاخیر سرسام‌آوری ایجاد می‌کند. در این سناریو، کاربر ثانیه‌های متمادی منتظر پاسخ می‌ماند، چون برای هر ابزار یک رفت‌وبرگشت کامل به مدل و شبکه صورت می‌گیرد. راهکار مدرن ارائه‌دهندگان پیشرو، فراخوانی موازی ابزارها (Parallel Tool Calling) است تا چندین دستور در یک پیام تولید و اجرا شوند.

با این حال، موازی‌سازی چالش‌های فنی پنهانی دارد. داده‌هایی که از طریق استریم ارسال می‌شوند تکه‌تکه هستند و رسیدن آن‌ها ترتیب مشخصی ندارد. در این درس یاد می‌گیرید چگونه رشته‌های ناقص JSON را مدیریت کنید، تکه‌ها را بر اساس شناسه‌ی فراخوانی تطبیق دهید و تاخیر نهایی سیستم را به کمترین حد ممکن برسانید.

اجرای متوالی سه درخواست مستقل برای بررسی آب‌وهوا یعنی سه رفت‌وبرگشت کامل. اگر آن‌ها را موازی اجرا کنید، کل زمان اجرا به اندازه‌ی کندترین فراخوانی کاهش می‌یابد. امروزه همه‌ی ارائه‌دهندگان پیشرو می‌توانند در یک نوبت گفت‌وگو (Turn) چندین فراخوانی ابزار (Tool call) صادر کنند. دستاورد این کار واقعی است، اما پیاده‌سازی فنی‌اش ظرافت‌های خاص خود را دارد. در این درس به هر دو بخش می‌پردازیم: توزیع موازی (Parallel fan-out) و سرهم‌کردن آرگومان‌های استریم‌شده، با تاکید ویژه بر تله‌ی تطبیق شناسه‌ها (Id correlation).

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

  • دلیل وجود parallel_tool_calls: true و زمان غیرفعال کردن آن را توضیح دهید.
  • تکه‌های استریم‌شده‌ی آرگومان‌ها را هنگام توزیع موازی به شناسه‌ی درست فراخوانی ابزار ربط دهید.
  • رشته‌های ناقص arguments را بدون پارس زودهنگام، به JSON کامل تبدیل کنید.
  • یک بنچمارک آب‌وهوا برای سه شهر اجرا کنید تا تفاوت تاخیر (Latency) در حالت متوالی و موازی را ببینید.

صورت مسئله

بدون فراخوانی‌های موازی، ایجنتی (Agent) که به سوال «آب‌وهوای بنگلور، توکیو و زوریخ چطور است؟» پاسخ می‌دهد، چنین روندی را طی می‌کند:

user -> LLM
LLM -> call get_weather(Bengaluru)
host -> run executor, reply with result
LLM -> call get_weather(Tokyo)
host -> run executor, reply with result
LLM -> call get_weather(Zurich)
host -> run executor, reply with result
LLM -> final text answer

سه رفت‌وبرگشت با مدل زبانی، که در هر کدام تاخیر مجری (Executor latency) هم اضافه می‌شود؛ یعنی تقریباً 4 برابر زمان واقعی ایده‌آل.

با فراخوانی‌های موازی:

user -> LLM
LLM -> call get_weather(Bengaluru); call get_weather(Tokyo); call get_weather(Zurich)
host -> run all three executors concurrently, reply with three results
LLM -> final text answer

تنها یک رفت‌وبرگشت با مدل زبانی. زمان مجری برابر با بیشینه‌ی زمان این سه درخواست است، نه مجموع آن‌ها. بنچمارک‌های عملیاتی روی OpenAI، Anthropic و Gemini نشان می‌دهند که زمان واقعی در بارهای کاری توزیع‌شده بین 60 تا 70 درصد کاهش می‌یابد.

هزینه‌ی این کار، پیچیدگی تطبیق شناسه‌هاست. وقتی این سه فراخوانی بدون ترتیب زمانی مشخص تمام می‌شوند، نتایج شما باید حاوی tool_call_id متناظر باشند تا مدل بتواند آن‌ها را کنار هم بچیند. هنگام استریمینگ نتایج، باید تکه‌های ناقص آرگومان‌ها را پیش از اجرا به یک JSON کامل تبدیل کنید. Gemini 3 شناسه‌های یکتا را تا حدی برای حل مشکلی واقعی اضافه کرد: در نسخه‌های قبلی، تشخیص دو فراخوانی موازی از یک ابزار یکسان ناممکن بود.

مفاهیم پایه

فعال‌سازی حالت موازی

  • OpenAI: گزینه‌ی parallel_tool_calls: true به‌طور پیش‌فرض فعال است. برای اجبار به اجرای متوالی، آن را روی false بگذارید.
  • Anthropic: اجرای موازی از طریق disable_parallel_tool_use: false کنترل می‌شود (در Claude 3.5 به بالا پیش‌فرض است). برای اجرای متوالی، مقدار true بدهید.
  • Gemini: همیشه قابلیت اجرای موازی دارد؛ با تنظیم tool_config.function_calling_config.mode = "AUTO" تصمیم‌گیری به مدل واگذار می‌شود.

زمانی حالت موازی را غیرفعال کنید که ابزارها به ترتیب اجرا وابستگی دارند (مثل create_file و سپس write_file)، خروجی یک فراخوانی ورودی دیگری را تعیین می‌کند، یا محدودیت نرخ درخواست (Rate limit) نمی‌تواند این حجم هم‌زمان را مدیریت کند.

تطبیق شناسه‌ها

هر فراخوانی صادرشده از مدل، یک id دارد. هر نتیجه‌ای که میزبان (Host) بازمی‌گرداند باید همان شناسه را داشته باشد، وگرنه نتایج مبهم خواهند بود.

  • OpenAI: فیلد tool_call_id روی هر پیام با نقش tool.
  • Anthropic: فیلد tool_use_id روی هر بلوک tool_result.
  • Gemini: فیلد id روی هر functionResponse (از Gemini 3 به بعد؛ در Gemini 2 تطبیق بر اساس نام تابع بود که در فراخوانی‌های هم‌نامِ موازی به مشکل می‌خورد).

اجرای هم‌زمان فراخوانی‌ها

میزبان، مجری هر فراخوانی را روی نخ (Thread)، کوروتین (Coroutine) یا ورکرِ راه‌دورِ (Remote worker) مستقل خودش اجرا می‌کند. ساده‌ترین بستر از یک استخر نخ (Thread pool) استفاده می‌کند؛ در محیط پروداکشن از asyncio همراه با asyncio.gather یا همروندی ساختاریافته (Structured concurrency) استفاده می‌شود. ترتیب اتمام کارها پیش‌بینی‌ناپذیر است — شناسه تنها ابزار شناسایی است.

یک باگ رایج: ارسال پاسخ‌ها به ترتیبی که در فهرست فراخوانی‌ها آمده‌اند، به‌جای ترتیبی که تمام شده‌اند. این کار معمولاً کار می‌کند، چون مدل فقط به tool_call_id اهمیت می‌دهد؛ اما اگر نتیجه‌ای جا بیفتد یا تکراری شود، پاسخی که ترتیب آن با ترتیب رسیدن نتایج فرق دارد، پیدا کردن شکاف را در لاگ‌ها بسیار سخت‌تر می‌کند. ترجیحاً نتایج را به ترتیب تکمیل و با شناسه‌های صریح برگردانید.

استریمینگ فراخوانی‌های ابزار

هنگام استریمینگ از مدل، arguments تکه‌تکه می‌رسند. سه جریان مجزا از تکه‌ها برای سه فراخوانی موازی روی سیم (شبکه) با هم ترکیب می‌شوند. برای هر شناسه به یک انباشت‌گر (Accumulator) نیاز دارید.

ساختار در ارائه‌دهندگان مختلف:

  • OpenAI: هر تکه به‌صورت choices[0].delta.tool_calls[i].function.arguments (رشته‌ی ناقص) می‌آید. این تکه حاوی index (موقعیت در فهرست فراخوانی‌ها) است. شما داده‌ها را به ازای هر ایندکس جمع می‌کنید، id را هنگام اولین نمایش می‌خوانید و وقتی finish_reason = "tool_calls" شد، رشته‌ی JSON را پارس می‌کنید.
  • Anthropic: رویدادهای استریم با message_start شروع می‌شوند، سپس یک content_block_start به ازای هر بلوک با نوع tool_use (شامل شناسه، نام و ورودی خالی) می‌آید. رویدادهای content_block_delta حامل تکه‌های input_json_delta هستند. در نهایت content_block_stop هر بلوک را می‌بندد.
  • Gemini: متد streamFunctionCallArguments (از Gemini 3 به بعد) تکه‌ها را همراه با یک functionCallId صادر می‌کند تا فراخوانی‌ها به‌خوبی در هم تنیده شوند. قبل از Gemini 3، استریمینگ در هر لحظه یک فراخوانی کامل را بازمی‌گرداند.

ساختار JSON ناقص و تله‌ی پارس زودهنگام

تا وقتی arguments کامل نشده است، نمی‌توانید آن را پارس کنید. یک JSON ناقص مانند {"city": "Beng معتبر نیست و خطا می‌اندازد. معیار درست، سیگنال پایان فراخوانی از سمت ارائه‌دهنده است: در OpenAI فیلد finish_reason = "tool_calls"، در Anthropic فیلد content_block_stop، یا رویداد پایان استریم در Gemini. فقط بعد از این سیگنال سراغ json.loads بروید. روشی پایدارتر استفاده از پارسر تدریجی JSON است که هم‌زمان با کامل شدن ساختار، خروجی می‌دهد؛ راهنمای استریمینگ OpenAI این روش را برای تجربه‌ی کاربری‌ای توصیه می‌کند که نشانگر زنده‌ی «در حال فکر کردن» دارد. شمردن آکولادها برای سنجش کامل بودن اصلاً قابل اعتماد نیست (آکولادهای داخل رشته‌های کوتیشن‌دار یا محتوای اسکیپ‌شده باعث خطا می‌شوند) و فقط باید در حد یک روش سرانگشتی برای اشکال‌زدایی غیررسمی استفاده شود.

اتمام نامرتب و بدون ترتیب

call_A: fast API, returns first
call_B: slow API, returns second
call_C: median API, returns third

پاسخ میزبان باز هم باید شناسه‌ها را ذکر کند:

[{role: "tool", tool_call_id: "call_A", content: ...},
 {role: "tool", tool_call_id: "call_B", content: ...},
 {role: "tool", tool_call_id: "call_C", content: ...}]

ترتیب آیتم‌ها در پاسخ، از نظر صحت عملکرد اهمیتی برای OpenAI یا Anthropic ندارد. Gemini هم تا زمانی که شناسه‌ها بخوانند، هر ترتیبی را قبول می‌کند.

بنچمارک: متوالی در برابر موازی

کد موجود در code/main.py سه مجری را با تاخیرهای 400، 600 و 800 میلی‌ثانیه‌ای شبیه‌سازی می‌کند. اجرای متوالی در مجموع 1800 میلی‌ثانیه طول می‌کشد. اجرای موازی در 800 = (400, 600, 800)max میلی‌ثانیه تمام می‌شود. این اختلاف مقداری ثابت است نه نسبی؛ بنابراین با افزایش تعداد ابزارها، صرفه‌جویی در زمان بیشتر می‌شود.

یک هشدار واقعی: فراخوانی‌های موازی به APIهای پایین‌دستی فشار می‌آورند. توزیع هم‌زمان 10 درخواست به سرویسی که محدودیت نرخ درخواست دارد شکست می‌خورد. در فاز 13 · 17 سازوکار فشار معکوس (Backpressure) در سطح گیت‌وی را بررسی می‌کنیم؛ منطق تلاش مجدد (Retry) هم برای فازهای بعدی برنامه‌ریزی شده است.

زمان واقعی در توزیع استریمینگ

اگر خود مدل خروجی را استریم کند، به‌جای معطل شدن برای نهایی شدن همه‌ی فراخوانی‌ها، می‌توانید اجرای هر فراخوانی را به محض کامل شدن آرگومان‌هایش کلید بزنید. این بهینه‌سازی‌ای (Optimization) است که OpenAI مستند کرده، اما همه‌ی SDKها آن را ارائه نمی‌دهند. کد این درس همین کار را می‌کند: به محض اینکه استریم شبیه‌سازی‌شده یک شیء آرگومانِ کامل بدهد، میزبان همان فراخوانی را شروع می‌کند.

انشعاب موازیسه فراخوانی، یک نوبت گفت‌وگو
PARALLEL FANOUTA single model turn can emit several independent tool calls at once. The host runs them concurrently, so total latency collapses from the sum of every call down to the slowest single one. Here three weather lookups finish on different clocks but the turn waits only for the longest.یک نوبتپاسخBengaluruget_weatherTokyoget_weatherZurichget_weather
یک نوبت گفت‌وگوی مدل می‌تواند چندین فراخوانی ابزار مستقل را هم‌زمان صادر کند. میزبان آن‌ها را به‌صورت همروند اجرا می‌کند، بنابراین تاخیر کل از مجموع تک‌تک فراخوانی‌ها به اندازه‌ی کندترین فراخوانی کاهش می‌یابد. در اینجا استعلام آب‌وهوا برای سه شهر در زمان‌های مختلفی تمام می‌شود اما نوبت گفت‌وگو فقط منتظر طولانی‌ترین آن‌ها می‌ماند.

استفاده از آموخته‌ها

فایل code/main.py دو بخش دارد. بخش اول سه فراخوانی شبیه‌سازی‌شده‌ی آب‌وهوا را با concurrent.futures.ThreadPoolExecutor به‌صورت متوالی و موازی اجرا می‌کند و زمان واقعی اجرای آن‌ها را در خروجی می‌نویسد. بخش دوم یک پاسخ استریمینگ ساختگی را پخش می‌کند — تکه‌های arguments برای سه فراخوانی موازی که روی یک استریم با هم تداخل دارند — و آن‌ها را به ازای هر شناسه با StreamAccumulator سرهم می‌کند. بدون نیاز به LLM یا شبکه؛ فقط منطق سرهم‌بندی را می‌بینید.

نکاتی که باید به آن‌ها دقت کنید:

  • زمان‌سنج اجرای متوالی به 1.8 ثانیه می‌رسد. زمان‌سنج اجرای موازی با همان تاخیرهای ساختگی عدد 0.8 ثانیه را ثبت می‌کند.
  • انباشت‌گر، تکه‌های نامرتب را با بافر کردن برای هر شناسه مدیریت می‌کند و فقط زمانی سراغ پارس می‌رود که JSON هر فراخوانی کامل شده باشد.
  • مجری به محض نهایی شدن آرگومان‌های یک شناسه شروع به کار می‌کند، نه پس از پایان کل استریم‌ها.

اجرای عملی

خروجی این درس فایل outputs/skill-parallel-call-safety-check.md است. این مهارت با گرفتن رجیستری ابزارها بررسی می‌کند کدام ابزارها برای اجرای موازی امن هستند، کدام‌ها وابستگی ترتیبی دارند و کدام‌ها محدودیت نرخ سرویس‌های پایین‌دستی را سرریز می‌کنند؛ سپس رجیستری بازبینی‌شده‌ای با پرچم‌های parallel_safe برای هر ابزار برمی‌گرداند.

تمرین‌ها

  1. اسکریپت code/main.py را اجرا کنید و تاخیرهای شبیه‌سازی‌شده را تغییر دهید. مطمئن شوید که نسبت موازی به متوالی تقریباً max/sum است (اجراهای واقعی به‌دلیل زمان‌بندی نخ‌ها، سریال‌سازی و سربار فریم‌ورک کمی با حالت ایده‌آل فرق دارند). در چه توزیعی از تاخیرها، اجرای موازی دیگر تاثیری ندارد؟

  2. انباشت‌گر را طوری تغییر دهید که حالت «لغو فراخوانی در میانه‌ی استریم» را با پاک کردن بافر آن شناسه و ارسال رویداد cancelled مدیریت کند. کدام ارائه‌دهنده این سناریو را صراحتاً مستند کرده است؟ رفتار content_block_stop در Anthropic و finish_reason: "length" در OpenAI را بررسی کنید.

  3. استخر نخ را با asyncio.gather جایگزین کنید. هر دو حالت را بنچمارک کنید. در حالت async باید به‌دلیل هزینه‌ی کمتر تعویض زمینه (Context switch) بهبود ناچیزی ببینید، البته فقط در صورتی که مجری‌ها I/O واقعی انجام دهند.

  4. دو ابزار را انتخاب کنید که نباید هم‌زمان اجرا شوند (مثلاً create_file و بعد write_file). گراف ordering_dependency را به رجیستری اضافه کرده و توزیع موازی را به این گراف مشروط کنید. این حداقل سازوکار لازم برای زمان‌بندی آگاه از وابستگی است، که فازهای آینده‌ی مهندسی ایجنت آن را قاعده‌مند می‌کنند.

  5. بخش فراخوانی موازی تابع (Function calling) در OpenAI و مستندات disable_parallel_tool_use در Anthropic را بخوانید. نوع ابزار واقعی‌ای را که Anthropic توصیه می‌کند موازی‌سازی‌اش را غیرفعال کنید مشخص کنید. (راهنمایی: تغییرات اثرگذار و حساس روی یک منبع مشترک.)

اصطلاحات کلیدی

اصطلاح در گفت‌وگوهای عامیانه معنای دقیق فنی
Parallel tool calls «توزیع در یک نوبت» ارسال چندین فراخوانی ابزار توسط مدل در یک پیام assistant
parallel_tool_calls «پرچم OpenAI» فعال یا غیرفعال کردن صدور چند فراخوانی هم‌زمان
disable_parallel_tool_use «پرچم برعکس Anthropic» پرچم انصراف؛ پیش‌فرض روی موازی فعال است
Tool call id «هندل تطبیق» شناسه‌ی ویژه‌ی هر فراخوانی که پیام نتیجه باید بازتاب دهد
Accumulator «بافر استریم» بافر رشته‌ای به ازای هر شناسه برای تکه‌های ناقص arguments
Out-of-order completion «سریع‌ترها اول» اتمام فراخوانی‌های موازی با ترتیب غیرقابل پیش‌بینی؛ شناسه‌ها نقش متصل‌کننده را دارند
Dependency graph «محدودیت‌های ترتیبی» ابزارهایی که خروجی یکی ورودی دیگری است؛ امکان اجرای موازی ندارند
Parse-early trap «ترکیدن JSON.parse» تلاش برای پارس کردن رشته‌ی ناقص arguments
streamFunctionCallArguments «قابلیت Gemini 3» ارسال استریم‌شده‌ی تکه‌های آرگومان با شناسه‌ی یکتا برای هر فراخوانی
Completion-order reply «معطل نماندن برای بقیه» ارسال نتایج به محض آماده‌شدن بر اساس شناسه

برای مطالعه‌ی بیشتر

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

چه زمانی باید فراخوانی موازی ابزارها را غیرفعال کنیم؟

زمانی که ابزارها وابستگی ترتیبی دارند (مثل ایجاد فایل و سپس نوشتن روی آن)، خروجی یک ابزار ورودی دیگری است، یا سرویس‌های مقصد محدودیت نرخ درخواست (Rate Limit) شدیدی دارند.

تله‌ی پارس زودهنگام (Parse-Early Trap) در استریمینگ ابزارها چیست؟

تلاش برای اجرای json.loads روی رشته‌های ناقص آرگومان پیش از رسیدن سیگنال پایان استریم (مانند finish_reason در OpenAI یا content_block_stop در Anthropic) که باعث پرتاب استثنا و شکست فرایند می‌شود.

شناسه‌ی فراخوانی ابزار (Tool Call ID) چه نقشی در اجرای موازی دارد؟

چون ابزارهای موازی ممکن است جابه‌جا و بدون ترتیب تمام شوند، مدل برای تطبیق هر نتیجه با دستور متناظرش منحصراً به شناسه متکی است و ترتیب بازگشت در آرایه اهمیتی ندارد.

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

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