معالجة استدعاءات الأدوات في الاستجابات المتدفقة
حلّلوا الاستجابات المتدفقة التي تصل فيها معاملات استدعاء الدوال رمزًا تلو الآخر، وخزّنوا أجزاء JSON مؤقتًا، وفعّلوا تنفيذ الأداة فقط عند اكتمال الاستدعاء.
معالجة استدعاءات الأدوات في الاستجابات المتدفقة درس مجاني في AI Engineering Academy على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في AI Engineering Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.
تصل استدعاءات الأدوات بشكل مختلف في التدفقات
عندما يقرر LLM استدعاء دالة، يتغير هيكل الاستجابة. فبدلاً من سلسلة content، يحتوي delta على مصفوفة tool_calls. لكن في الاستجابة المُبثّة، تصل وسيطات استدعاء الدالة رمزاً تلو الآخر على شكل سلسلة JSON جزئية؛ فلا تتلقون كائن JSON كاملاً في كتلة واحدة. يجب تخزين هذه الأجزاء مؤقتاً وإعادة تجميع JSON الكامل قبل أن تتمكنوا من تحليله وتنفيذ استدعاء الأداة.
# In a non-streaming response, tool call is complete:
# choice.message.tool_calls[0].function.arguments = '{"city": "Paris"}'
# In a streaming response, arguments arrive in pieces:
# chunk 1: delta.tool_calls[0].function.arguments = '{'
# chunk 2: delta.tool_calls[0].function.arguments = '"city"'
# chunk 3: delta.tool_calls[0].function.arguments = ': "'
# chunk 4: delta.tool_calls[0].function.arguments = 'Paris'
# chunk 5: delta.tool_calls[0].function.arguments = '"}'
# You must concatenate these before JSON.parse can workاكتشاف استدعاء أداة في التدفق
تحقق من finish_reason في كل جزء لمعرفة متى تتوقع استدعاءات الأدوات. عندما تكون قيمة finish_reason هي 'tool_calls'، يكون النموذج قد قرر استدعاء دالة ويكون البث على وشك الانتهاء. وعندما تكون قيمة finish_reason هي 'stop'، يكون النموذج قد أنتج استجابة نصية عادية. أثناء البث، تحقق مما إذا كانت chunk.choices[0].delta.tool_calls غير مساوية لـ None للتعرّف على أجزاء وسيطات استدعاء الأداة.
async def detect_stream_type(messages, tools):
stream = await async_client.chat.completions.create(
model='gpt-4o-mini',
messages=messages,
tools=tools,
stream=True,
)
response_type = 'text'
async for chunk in stream:
choice = chunk.choices[0]
if choice.delta.tool_calls: # tool call fragment arriving
response_type = 'tool_call'
if choice.finish_reason == 'tool_calls':
print('Model wants to call a function')
elif choice.finish_reason == 'stop':
print('Normal text response')
return response_typeتخزين مؤقت لوسيطات استدعاء الأدوات
استخدم قاموسًا مفاتيحه فهارس استدعاءات الأدوات لتجميع أجزاء الوسيطات من كل جزء. يحدد الفهرس استدعاء الأداة الذي يجري بثه — فقد يستدعي النموذج عدة دوال في استجابة واحدة. لكل جزء يحتوي على دلتا tool_calls غير فارغة، أضف جزء الوسيطة إلى الإدخال المناسب في المخزن المؤقت، والمفهرس بفهرس استدعاء الأداة.
from collections import defaultdict
async def collect_streamed_tool_calls(messages, tools):
stream = await async_client.chat.completions.create(
model='gpt-4o-mini',
messages=messages,
tools=tools,
stream=True,
)
tool_call_buffers = defaultdict(lambda: {'name': '', 'id': '', 'arguments': ''})
text_buffer = ''
async for chunk in stream:
delta = chunk.choices[0].delta
if delta.content: # text content
text_buffer += delta.content
if delta.tool_calls:
for tc in delta.tool_calls:
idx = tc.index
if tc.id:
tool_call_buffers[idx]['id'] = tc.id
if tc.function.name:
tool_call_buffers[idx]['name'] += tc.function.name
if tc.function.arguments:
tool_call_buffers[idx]['arguments'] += tc.function.arguments
return text_buffer, dict(tool_call_buffers)تحليل استدعاءات الأدوات وتنفيذها
بعد انتهاء البث وحصولك على سلاسل الوسيطات الكاملة، حلّل كل سلسلة باستخدام json.loads ووجّهها إلى دالة Python المناسبة. نفّذ استدعاءات الأدوات بالترتيب الذي طُلبت به (أو بالتوازي إذا كانت مستقلة)، ثم نسّق النتائج على هيئة رسائل استجابة للأداة لإرسالها في استدعاء API اللاحق.
import json
# Example tool registry
tools_registry = {
'get_weather': lambda city, unit='celsius': {'temp': 22, 'desc': 'sunny', 'city': city},
'search_docs': lambda query, top_k=3: [{'title': 'Doc 1', 'snippet': 'Relevant info...'}],
}
def execute_tool_calls(tool_call_buffers: dict) -> list[dict]:
tool_messages = []
for idx in sorted(tool_call_buffers.keys()):
tc = tool_call_buffers[idx]
func_name = tc['name']
args = json.loads(tc['arguments'])
if func_name in tools_registry:
result = tools_registry[func_name](**args)
else:
result = {'error': f'Unknown function: {func_name}'}
tool_messages.append({
'role': 'tool',
'tool_call_id': tc['id'],
'content': json.dumps(result),
})
return tool_messagesحلقة بث استدعاءات الأدوات الكاملة
يتطلب التدفق الكامل لبث استدعاءات الأدوات حلقة محادثة متعددة الأدوار. قد يعيد الطلب الأول استدعاءات أدوات؛ عندها تنفذها وتضيف النتائج إلى سجل الرسائل، ثم يعيد الطلب الثاني الإجابة النصية النهائية. وقد تتكرر هذه الحلقة عدة مرات إذا اختار النموذج إجراء استدعاءات أدوات إضافية استنادًا إلى نتائج الاستدعاءات السابقة.
async def streaming_agent_loop(initial_messages, tools):
messages = list(initial_messages)
max_iterations = 5
for iteration in range(max_iterations):
text, tool_calls = await collect_streamed_tool_calls(messages, tools)
if tool_calls:
# Append assistant message with tool calls
assistant_msg = {
'role': 'assistant',
'content': text or None,
'tool_calls': [
{'id': tc['id'], 'type': 'function',
'function': {'name': tc['name'], 'arguments': tc['arguments']}}
for tc in tool_calls.values()
]
}
messages.append(assistant_msg)
# Execute tools and append results
tool_results = execute_tool_calls(tool_calls)
messages.extend(tool_results)
else:
# No more tool calls — final text response
print('Final answer:', text)
return text
return 'Max iterations reached'بث النص مع تخزين استدعاءات الأدوات مؤقتًا
عمليًا، تريد بث النص إلى العميل فورًا بالتزامن مع تخزين أي وسيطات لاستدعاءات الأدوات مؤقتًا. ويتطلب ذلك التمييز بين الأجزاء التي تحمل content (لبثها فورًا) والأجزاء التي تحمل tool_calls (لتخزينها مؤقتًا من أجل تنفيذها لاحقًا). ولا يمكنك تنفيذ الأدوات ومتابعة العملية إلا بعد انتهاء البث واكتمال استدعاءات الأدوات.
async def stream_with_tools(messages, tools):
stream = await async_client.chat.completions.create(
model='gpt-4o-mini', messages=messages, tools=tools, stream=True
)
tool_buffers = defaultdict(lambda: {'name': '', 'id': '', 'arguments': ''})
text_parts = []
async for chunk in stream:
delta = chunk.choices[0].delta
finish = chunk.choices[0].finish_reason
if delta.content:
text_parts.append(delta.content)
yield ('text', delta.content) # stream to client immediately
if delta.tool_calls:
for tc in delta.tool_calls:
if tc.id: tool_buffers[tc.index]['id'] = tc.id
if tc.function.name: tool_buffers[tc.index]['name'] += tc.function.name
if tc.function.arguments: tool_buffers[tc.index]['arguments'] += tc.function.arguments
if finish == 'tool_calls':
yield ('tool_calls', dict(tool_buffers)) # signal tool execution neededتنفيذ استدعاءات الأدوات بالتوازي
عندما يعيد النموذج عدة استدعاءات أدوات في الوقت نفسه (وهي ميزة تُسمى استدعاء الدوال بالتوازي)، نفّذها بالتوازي باستخدام asyncio.gather بدلًا من تنفيذها بالتتابع. إذ يضيف التنفيذ المتتابع زمن انتظار غير ضروري — فإذا استدعى النموذج واجهة طقس وجرى بحث في قاعدة بيانات في الوقت نفسه، فلا يوجد سبب لانتظار انتهاء أحدهما قبل بدء الآخر.
import asyncio
async def execute_tool_calls_parallel(tool_call_buffers: dict) -> list[dict]:
async def execute_one(idx, tc):
func_name = tc['name']
args = json.loads(tc['arguments'])
if func_name in async_tools_registry:
result = await async_tools_registry[func_name](**args)
else:
result = {'error': f'Unknown function: {func_name}'}
return {
'role': 'tool',
'tool_call_id': tc['id'],
'content': json.dumps(result),
}
tasks = [execute_one(idx, tc) for idx, tc in sorted(tool_call_buffers.items())]
return await asyncio.gather(*tasks)بث الإجابة النهائية بعد استخدام الأدوات
بعد تنفيذ استدعاءات الأدوات وإضافة النتائج إلى سجل الرسائل، أجرِ طلب بث ثانٍ للحصول على الإجابة النهائية من النموذج. وابث هذه الاستجابة مباشرةً إلى العميل. يُعد هذا النمط المكوّن من طلبين (طلب أولي يتضمن استدعاءات الأدوات + طلب لاحق يتضمن نتائج الأدوات) دورة الدور القياسية للوكيل، ويمكن لكلا الطلبين بث النص إلى واجهة المستخدم.
async def full_tool_calling_stream(question: str, tools: list):
messages = [{'role': 'user', 'content': question}]
# First request: may produce tool calls
tool_buffers = {}
text1 = ''
async for event_type, data in stream_with_tools(messages, tools):
if event_type == 'text':
text1 += data
yield data # stream partial text if any
elif event_type == 'tool_calls':
tool_buffers = data
if tool_buffers:
# Execute tools, then get final streaming answer
tool_results = await execute_tool_calls_parallel(tool_buffers)
messages += [{ # assistant tool call message
'role': 'assistant',
'tool_calls': [
{'id': tc['id'], 'type': 'function',
'function': {'name': tc['name'], 'arguments': tc['arguments']}}
for tc in tool_buffers.values()
]
}] + tool_results
# Second request: final answer streams directly
async for token in token_stream(messages): # from earlier lesson
yield tokenعرض تقدم استدعاء الأدوات في واجهة المستخدم
ينبغي أن يرى المستخدمون ما الذي ينفذه الوكيل أثناء انتظار اكتمال استدعاءات الأدوات. قبل تنفيذ الأدوات، ابث حدث حالة إلى العميل يوضح الدالة التي يجري استدعاؤها والوسيطات المستخدمة معها. وبعد التنفيذ، ابث حالة تفيد باكتمال العملية. وتُحسّن هذه الشفافية الاستجابة الملحوظة بدرجة كبيرة، كما تساعد المستخدمين على تصحيح استخدام الأدوات غير المتوقع.
import json
async def stream_with_progress(question, tools):
messages = [{'role': 'user', 'content': question}]
tool_buffers = {}
async for event_type, data in stream_with_tools(messages, tools):
if event_type == 'tool_calls':
tool_buffers = data
for tc in tool_buffers.values():
args = json.loads(tc['arguments'])
yield f'data: {json.dumps({"type": "tool_start", "function": tc["name"], "args": args})}\n\n'
result = tools_registry.get(tc['name'], lambda **kw: {})(** args)
yield f'data: {json.dumps({"type": "tool_done", "function": tc["name"]})}\n\n'
# Then stream final answer...معالجة الأخطاء في بث استدعاءات الأدوات
قد يفشل تنفيذ الأدوات — فقد تعيد واجهات API أخطاء، أو ترفع الدوال استثناءات، أو يفشل تحليل JSON. احرص دائمًا على التقاط الاستثناءات أثناء تنفيذ الأدوات وإعادة استجابة خطأ منظّمة إلى النموذج. ويمكن للنموذج عندئذٍ أن يقرر إعادة المحاولة باستخدام وسيطات مختلفة، أو استدعاء أداة بديلة، أو توضيح فشل الإجراء المطلوب للمستخدم. ولا تسمح مطلقًا لاستثناء أداة غير ملتقط بالتسبب في تعطل حلقة البث.
def safe_execute_tool(func_name: str, args: dict) -> str:
try:
if func_name not in tools_registry:
return json.dumps({'error': f'Function {func_name!r} not found'})
result = tools_registry[func_name](**args)
return json.dumps(result)
except TypeError as e:
return json.dumps({'error': f'Invalid arguments: {str(e)}'})
except Exception as e:
return json.dumps({'error': f'Execution failed: {str(e)}'})
# Tool result message with error handled
tool_message = {
'role': 'tool',
'tool_call_id': tc['id'],
'content': safe_execute_tool(tc['name'], json.loads(tc['arguments'])),
}مقارنة البث مع عدم البث للوكلاء
في التطبيقات الوكيلة، يضيف البث تعقيدًا لكنه يقدم قيمة كبيرة لتجربة المستخدم. فمن دون البث، لن يرى المستخدم شيئًا أثناء حلقة متعددة الخطوات لاستدعاء الأدوات قد تستغرق من 10 إلى 30 ثانية. أما مع البث، فسيرى النصوص الوسيطة وإشعارات استدعاء الأدوات والإجابة النهائية تظهر رمزًا بعد رمز. ويستحق التعقيد الإضافي في الشيفرة ذلك عادةً في التطبيقات التفاعلية، بينما يمكن للوكلاء الذين يعملون تلقائيًا في الخلفية استخدام عدم البث للحصول على شيفرة أبسط.
تحقق سريع
اختبر مدى فهمك لاستدعاءات الأدوات في الاستجابات المبثوثة من هذا الدرس.
مراجعة الدرس
تعلمت في هذا الدرس أن وسيطات استدعاء الأدوات تصل على هيئة أجزاء JSON في الاستجابات المبثوثة، ويجب تخزينها مؤقتًا حسب فهرس استدعاء الأداة قبل تحليلها، وأنه يجب تنفيذ الأدوات بعد اكتمال البث ثم إجراء طلب بث ثانٍ للحصول على الإجابة النهائية، وأن التنفيذ المتوازي باستخدام asyncio.gather يقلل زمن الانتظار عندما يستدعي النموذج عدة دوال في الوقت نفسه. احرص دائمًا على التقاط أخطاء تنفيذ الأدوات لمنع تعطل حلقة الوكيل. سننتقل بعد ذلك إلى تنفيذ التخزين المؤقت للاستجابات لتقليل تكاليف API.
تعلم Python مع معلم ذكاء اصطناعي — مجانًا
اكتب وقم بتشغيل أكوادك الفعلية في المتصفح، واحصل على مساعدة فورية من معلم ذكاء اصطناعي متاح 24/7، واستمر من حيث توقفت على الويب أو في التطبيق.
- الدورات
- 30
- الدروس
- 120
الأسئلة الشائعة
هل درس «معالجة استدعاءات الأدوات في الاستجابات المتدفقة» مجاني؟
نعم — نص درس «معالجة استدعاءات الأدوات في الاستجابات المتدفقة» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة AI Engineering Academy، انتقل إلى CoddyKit PRO. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.
ماذا ستتعلم في «معالجة استدعاءات الأدوات في الاستجابات المتدفقة»؟
حلّلوا الاستجابات المتدفقة التي تصل فيها معاملات استدعاء الدوال رمزًا تلو الآخر، وخزّنوا أجزاء JSON مؤقتًا، وفعّلوا تنفيذ الأداة فقط عند اكتمال الاستدعاء. تتمرن على AI Engineering Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ AI Engineering Academy؟
لا تُشترط خبرة سابقة. AI Engineering Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «معالجة استدعاءات الأدوات في الاستجابات المتدفقة»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس AI Engineering Academy هذا؟
نعم. كل درس في AI Engineering Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- فهم بث الرموز
- استهلاك التدفقات باستخدام Python SDK
- البث في FastAPI باستخدام Server-Sent Events
- معالجة استدعاءات الأدوات في الاستجابات المتدفقة