وقتی یک ایجنت هوش مصنوعی نیاز دارد اطلاعات چند شهر یا منبع مختلف را بررسی کند، اجرای پیاپی و متوالی این درخواستها تاخیر سرسامآوری ایجاد میکند. در این سناریو، کاربر ثانیههای متمادی منتظر پاسخ میماند، چون برای هر ابزار یک رفتوبرگشت کامل به مدل و شبکه صورت میگیرد. راهکار مدرن ارائهدهندگان پیشرو، فراخوانی موازی ابزارها (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ها آن را ارائه نمیدهند. کد این درس همین کار را میکند: به محض اینکه استریم شبیهسازیشده یک شیء آرگومانِ کامل بدهد، میزبان همان فراخوانی را شروع میکند.
استفاده از آموختهها
فایل code/main.py دو بخش دارد. بخش اول سه فراخوانی شبیهسازیشدهی آبوهوا را با concurrent.futures.ThreadPoolExecutor بهصورت متوالی و موازی اجرا میکند و زمان واقعی اجرای آنها را در خروجی مینویسد. بخش دوم یک پاسخ استریمینگ ساختگی را پخش میکند — تکههای arguments برای سه فراخوانی موازی که روی یک استریم با هم تداخل دارند — و آنها را به ازای هر شناسه با StreamAccumulator سرهم میکند. بدون نیاز به LLM یا شبکه؛ فقط منطق سرهمبندی را میبینید.
نکاتی که باید به آنها دقت کنید:
- زمانسنج اجرای متوالی به 1.8 ثانیه میرسد. زمانسنج اجرای موازی با همان تاخیرهای ساختگی عدد 0.8 ثانیه را ثبت میکند.
- انباشتگر، تکههای نامرتب را با بافر کردن برای هر شناسه مدیریت میکند و فقط زمانی سراغ پارس میرود که JSON هر فراخوانی کامل شده باشد.
- مجری به محض نهایی شدن آرگومانهای یک شناسه شروع به کار میکند، نه پس از پایان کل استریمها.
اجرای عملی
خروجی این درس فایل outputs/skill-parallel-call-safety-check.md است. این مهارت با گرفتن رجیستری ابزارها بررسی میکند کدام ابزارها برای اجرای موازی امن هستند، کدامها وابستگی ترتیبی دارند و کدامها محدودیت نرخ سرویسهای پاییندستی را سرریز میکنند؛ سپس رجیستری بازبینیشدهای با پرچمهای parallel_safe برای هر ابزار برمیگرداند.
تمرینها
-
اسکریپت
code/main.pyرا اجرا کنید و تاخیرهای شبیهسازیشده را تغییر دهید. مطمئن شوید که نسبت موازی به متوالی تقریباًmax/sumاست (اجراهای واقعی بهدلیل زمانبندی نخها، سریالسازی و سربار فریمورک کمی با حالت ایدهآل فرق دارند). در چه توزیعی از تاخیرها، اجرای موازی دیگر تاثیری ندارد؟ -
انباشتگر را طوری تغییر دهید که حالت «لغو فراخوانی در میانهی استریم» را با پاک کردن بافر آن شناسه و ارسال رویداد
cancelledمدیریت کند. کدام ارائهدهنده این سناریو را صراحتاً مستند کرده است؟ رفتارcontent_block_stopدر Anthropic وfinish_reason: "length"در OpenAI را بررسی کنید. -
استخر نخ را با
asyncio.gatherجایگزین کنید. هر دو حالت را بنچمارک کنید. در حالت async باید بهدلیل هزینهی کمتر تعویض زمینه (Context switch) بهبود ناچیزی ببینید، البته فقط در صورتی که مجریها I/O واقعی انجام دهند. -
دو ابزار را انتخاب کنید که نباید همزمان اجرا شوند (مثلاً
create_fileو بعدwrite_file). گرافordering_dependencyرا به رجیستری اضافه کرده و توزیع موازی را به این گراف مشروط کنید. این حداقل سازوکار لازم برای زمانبندی آگاه از وابستگی است، که فازهای آیندهی مهندسی ایجنت آن را قاعدهمند میکنند. -
بخش فراخوانی موازی تابع (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 | «معطل نماندن برای بقیه» | ارسال نتایج به محض آمادهشدن بر اساس شناسه |
برای مطالعهی بیشتر
- OpenAI — Parallel function calling — رفتار پیشفرض و پرچم انصراف
- Anthropic — Parallel tool use — پارامتر
disable_parallel_tool_useو ارسال دستهای نتایج - Google — Gemini function calling parallel section — فراخوانیهای موازی متکی بر شناسه در Gemini 3
- OpenAI — Streaming responses with tools — سرهمکردن تکههای آرگومان در استریمهای OpenAI
- Anthropic — Streaming messages — رویداد
content_block_deltaهمراه باinput_json_delta
