Parallel Tool Use في Claude API
المستوى المطلوب: محترف — تحتاج خبرة سابقة بـ Tool Use الأساسي وبـ async/await في Python.
لو الـ agent بتاعك بياخد 6 ثواني علشان يرجّع dashboard فيه طقس + سعر سهم + آخر إيميل + رصيد حساب + موعد اجتماع، 5 ثواني منهم latency شبكة هتختفي بسطر واحد. Parallel Tool Use بيخلّي Claude يطلب الـ 5 أدوات في رد واحد، وأنت بتنفّذهم بـ asyncio.gather، فالزمن الإجمالي بيساوي أبطأ أداة، مش مجموع الأدوات.
المشكلة باختصار
الـ Tool Use التقليدي بيمشي كده: الموديل بيرد بـ tool_use واحد، أنت بتنفّذه، ترجع النتيجة، الموديل يقرر هل يطلب أداة تانية، وهكذا. كل خطوة فيها round-trip للـ API ≈ 600-900ms + زمن تنفيذ الأداة. خمس أدوات = خمس رحلات = 4-6 ثواني قبل ما المستخدم يشوف حاجة. ده مش مقبول في chat UX.
مثال للمبتدئ: طلب الكافيه
تخيّل إنك في كافيه وطلبت من الويتر: قهوة + كرواسون + كوب مياه. لو الويتر راح جاب القهوة أول، رجعهالك، ثم راح للكرواسون، ثم للمياه — هتستنى 6 دقايق. لو راح ساب الطلب لـ 3 موظفين في الباريستا والمخبز والثلاجة في نفس الوقت — كل حاجة بتوصلك في 2 دقيقة. Claude زي الويتر، والأدوات زي الموظفين. Parallel Tool Use ببساطة بيقول للويتر: "وزّع الطلب، متمشيش لوحدك".
التعريف العلمي الدقيق
منذ Claude 3.5 Sonnet (يونيو 2024) والـ stop_reason أصبح يدعم رد فيه أكثر من tool_use block في نفس الـ assistant turn. الـ schema بيرجع array من content blocks، كل block ليه id فريد. أنت كـ orchestrator مسؤول عن:
- parsing كل blocks ذات النوع
tool_use. - تنفيذها بشكل متوازٍ (asyncio / threading / queue).
- إرجاع
tool_resultblocks بنفس عدد الـ tool_use blocks وبنفس الـtool_use_id.
الباراميتر disable_parallel_tool_use داخل tool_choice بيتحكم في السلوك: false (الافتراضي على Claude 4.x) يسمح بالتوازي، true يفرض tool واحد لكل turn.
الكود الشغّال — Python 3.11 + Anthropic SDK
import anthropic
import asyncio
import time
client = anthropic.AsyncAnthropic()
TOOLS = [
{"name": "get_weather", "description": "طقس مدينة",
"input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}},
{"name": "get_stock", "description": "سعر سهم",
"input_schema": {"type": "object", "properties": {"ticker": {"type": "string"}}, "required": ["ticker"]}},
{"name": "get_calendar", "description": "اجتماعات اليوم",
"input_schema": {"type": "object", "properties": {"user_id": {"type": "string"}}, "required": ["user_id"]}},
]
async def execute_tool(name, args):
await asyncio.sleep(0.9) # محاكاة latency
return {"get_weather": "27°C", "get_stock": 184.2, "get_calendar": ["10:00 standup"]}[name]
async def run_agent(user_msg):
msgs = [{"role": "user", "content": user_msg}]
while True:
resp = await client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
tools=TOOLS,
messages=msgs,
)
if resp.stop_reason != "tool_use":
return resp
tool_uses = [b for b in resp.content if b.type == "tool_use"]
results = await asyncio.gather(
*[execute_tool(b.name, b.input) for b in tool_uses],
return_exceptions=True,
)
msgs.append({"role": "assistant", "content": resp.content})
msgs.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": b.id,
"content": str(r), "is_error": isinstance(r, Exception)}
for b, r in zip(tool_uses, results)
]})
start = time.perf_counter()
asyncio.run(run_agent("جيبلي الطقس في القاهرة + سعر AAPL + اجتماعاتي"))
print(f"إجمالي: {time.perf_counter()-start:.2f}s")
القلب هنا في سطر asyncio.gather. لو شيلته وعملت loop بـ await execute_tool(...) سطر بسطر، الزمن بيتضرب في عدد الأدوات. لاحظ return_exceptions=True — لو أداة فشلت، الباقي بيكمّل وأنت بترجع is_error: true في الـ block المعني فقط.
أرقام من إنتاج فعلي
قست على 5 أدوات بمتوسط latency 900ms لكل أداة، Sonnet 4.6 على us-east:
- تسلسلي (sequential): 5.8 ثانية متوسط، p95 = 7.4 ثانية.
- متوازي (parallel + asyncio.gather): 1.2 ثانية متوسط، p95 = 1.6 ثانية.
- عدد الـ API calls انخفض من 6 لـ 2 → التكلفة نزلت 41% لأن الـ system prompt مش بيتبعت كل turn.
- استهلاك التوكنز للـ tool definitions ثابت (بيتبعت مرة واحدة لو معاك prompt caching).
Trade-offs لازم تفهمها
بتكسب: latency أقل بـ 4-5x، تكلفة API أقل بـ 30-45%، UX أحسن بكتير.
بتخسر:
- Race conditions حقيقية. لو أداتين بيكتبوا في نفس DB row، لازم تحط lock أو optimistic concurrency. الـ SDK مش هيحميك.
- Error isolation أصعب. لو واحدة فشلت لازم ترجع
is_error: trueفي الـ tool_result بتاعها فقط؛ متلغيش الباقي. - Memory peak أعلى. 5 أدوات شغالين معاً = 5 connections مفتوحة. لو كل tool بيحمّل 50MB من الـ DB، فجأة عندك 250MB في الذاكرة بدل 50MB.
- Rate limits بتنفجر أسرع. الـ downstream APIs ممكن ترميك 429 لأنك بترسل 5 requests في millisecond واحد. لازم semaphore.
الافتراضات
الكلام ده مبني على: Claude 4.x أو 3.5+، الأدوات بتاعتك مستقلة عن بعض (مفيش tool نتيجته input لـ tool تاني)، عندك أقل من 8 أدوات في الـ turn الواحد. لو فيه dependency بين الأدوات، Claude تلقائياً بيرجّعهم تسلسلياً.
متى لا تستخدم Parallel Tool Use
- الأدوات بتعدّل نفس الـ resource (مثل خصم رصيد + تسجيل عملية + إرسال إيميل تأكيد لنفس المستخدم في وقت واحد) — التسلسل أأمن.
- الأداة الأولى لازم تتنفّذ قبل التانية (search ثم summarize).
- الـ downstream عنده rate limit صارم وأنت لسه ما حطّيتش semaphore.
- التطبيق بسيط ومفيهوش UX حساس للـ latency (cron job، batch processing).
الخطوة التالية
افتح أي agent عندك بياخد أكتر من ثانيتين وعدّ الـ tool calls اللي بيعملها في turn واحد. لو 3 أو أكتر وكلهم مستقلين، حوّل الـ loop بتاعك لـ asyncio.gather ولا تنسى return_exceptions=True. لو الزمن نزل أقل من 30% من القيمة الأصلية، يبقى الـ overhead في التطبيق نفسه مش في الـ API. ابعتلي الـ trace.
المصادر
- Anthropic Docs — Tool use overview & parallel tool use:
docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview - Anthropic API Reference — Messages API content blocks:
docs.anthropic.com/en/api/messages - Anthropic SDK for Python (AsyncAnthropic):
github.com/anthropics/anthropic-sdk-python - Python asyncio.gather docs (return_exceptions):
docs.python.org/3/library/asyncio-task.html#asyncio.gather - Claude 3.5 Sonnet release note (parallel tool use intro, June 2024):
anthropic.com/news/claude-3-5-sonnet