وكلاء الذكاء الاصطناعي من الصفر: وكلاء فرديون

وكلاء الذكاء الاصطناعي (AI Agents) هي برامج مستقلة تُنفّذ المهام، وتتخذ القرارات، وتتواصل مع الآخرين. عادةً، تستخدم هذه الوكلاء مجموعة من الأدوات لإنجاز المهام. في تطبيقات GenAI، تُعالج هذه الوكلاء التفكير المتسلسل، ويمكنها استخدام أدوات خارجية (مثل عمليات البحث على الويب أو استعلامات قواعد البيانات) عندما لا تكفي معرفة نموذج لغوي كبير. على عكس روبوتات الدردشة البسيطة، التي تُولّد نصوصًا عشوائية عند عدم التأكد، يُفعّل وكيل الذكاء الاصطناعي الأدوات لتقديم استجابات أكثر دقةً وتحديدًا.

نقترب أكثر فأكثر من مفهوم الذكاء الاصطناعي الوكيل: أنظمة تتمتع بمستوى أعلى من الاستقلالية والقدرة على اتخاذ القرارات، دون تدخل بشري مباشر. فبينما يستجيب عملاء الذكاء الاصطناعي اليوم بشكل تفاعلي للمدخلات البشرية، تتفاعل أنظمة الذكاء الاصطناعي الوكيل في المستقبل بشكل استباقي في حل المشكلات، ويمكنها تعديل سلوكها بناءً على الموقف.

اليوم، أصبح بناء الوكلاء من الصفر سهلاً، تمامًا كما كان تدريب نموذج الانحدار اللوجستي قبل عشر سنوات. في ذلك الوقت، وفّرت Scikit-Learn مكتبةً بسيطةً لتدريب نماذج التعلم الآلي بسرعة باستخدام بضعة أسطر من التعليمات البرمجية، مما يُلغي الكثير من التعقيد الكامن وراءها.

في هذا الدرس، سأوضح كيفية بناء أنواع مختلفة من وكلاء الذكاء الاصطناعي من الصفر، من الأنظمة البسيطة إلى الأنظمة الأكثر تقدمًا. سأقدم بعض أكواد بايثون المفيدة التي يمكن تطبيقها بسهولة في حالات مماثلة (فقط انسخ، الصق، ثم شغّل)، وسأشرح كل سطر من الكود مع التعليقات لتكرار هذا المثال.

الإعداد

كما ذكرتُ، يُمكن لأي شخص امتلاك وكيل مُخصّص يعمل محليًا مجانًا دون الحاجة إلى وحدات معالجة رسومية أو مفاتيح واجهة برمجة التطبيقات. المكتبة الضرورية الوحيدة هي Ollama (pip install ollama==0.4.7)، حيث تُتيح للمستخدمين تشغيل برامج LLM محليًا، دون الحاجة إلى خدمات سحابية، مما يُتيح تحكمًا أكبر في خصوصية البيانات والأداء.

أولاً، عليك تنزيل Ollama من الموقع.

بعد ذلك، على واجهة جهاز الكمبيوتر المحمول، استخدم الأمر لتنزيل برنامج LLM المحدد. سأستخدم Qwen من Alibaba، فهو ذكي وخفيف.

بعد اكتمال التنزيل، يمكنك الانتقال إلى Python والبدء في كتابة التعليمات البرمجية.

import ollama
llm = "qwen2.5"

دعونا نختبر LLM:

stream = ollama.generate(model=llm, prompt='''what time is it?''', stream=True)
for chunk in stream:
    print(chunk['response'], end='', flush=True)

من الواضح أن برنامج نموذج لغوي كبير محدودٌ جدًا، ولا يمكنه فعل الكثير سوى الدردشة. لذلك، علينا أن نمنحه إمكانية اتخاذ الإجراءات اللازمة، أو بمعنى آخر، تفعيل الأدوات.

من أكثر الأدوات شيوعًا إمكانية البحث في الإنترنت. في بايثون، أسهل طريقة للقيام بذلك هي باستخدام متصفح DuckDuckGo الخاص الشهير (pip install duckduckgo-search==6.3.5). يمكنك استخدام المكتبة الأصلية مباشرةً أو استيراد مُغلِّف LangChain (pip install langchain-community==0.3.17).

مع Ollama، لاستخدام أداة، يجب وصف الدالة في القاموس.

from langchain_community.tools import DuckDuckGoSearchResults
def search_web(query: str) -> str:
  return DuckDuckGoSearchResults(backend="news").run(query)
tool_search_web = {'type':'function', 'function':{
  'name': 'search_web',
  'description': 'Search the web',
  'parameters': {'type': 'object',
                'required': ['query'],
                'properties': {
                    'query': {'type':'str', 'description':'the topic or subject to search on the web'},
}}}}
## test
search_web(query="nvidia")

قد تكون عمليات البحث على الإنترنت واسعة جدًا، وأريد منح الوكيل خيارًا ليكون أكثر دقة. لنفترض أنني أخطط لاستخدام هذا الوكيل لمعرفة التحديثات المالية، لذا يمكنني تزويده بأداة مخصصة لهذا الموضوع، مثل البحث في موقع مالي فقط بدلاً من البحث في الإنترنت بأكمله.

def search_yf(query: str) -> str:
  engine = DuckDuckGoSearchResults(backend="news")
  return engine.run(f"site:finance.yahoo.com {query}")
tool_search_yf = {'type':'function', 'function':{
  'name': 'search_yf',
  'description': 'Search for specific financial news',
  'parameters': {'type': 'object',
                'required': ['query'],
                'properties': {
                    'query': {'type':'str', 'description':'the financial topic or subject to search'},
}}}}
## test
search_yf(query="nvidia")

وكيل بسيط (بحث الويب)

في رأيي، يجب أن يكون العميل الأساسي قادرًا على الأقل على الاختيار بين أداة واحدة أو اثنتين وإعادة صياغة إخراج الإجراء لإعطاء المستخدم إجابة مناسبة وموجزة.

أولاً، عليك كتابة موجه لوصف غرض الوكيل، وكلما كان أكثر تفصيلاً كان ذلك أفضل (الموجه الخاص بي عام جدًا)، وستكون هذه هي الرسالة الأولى في سجل الدردشة مع LLM.

prompt = '''You are an assistant with access to tools, you must decide when to use tools to answer user message.''' 
messages = [{"role":"system", "content":prompt}]

من أجل إبقاء المحادثة مع الذكاء الاصطناعي نشطة، سأستخدم حلقة تبدأ بإدخال المستخدم ثم يتم استدعاء الوكيل للرد (والذي يمكن أن يكون نصًا من LLM أو تنشيط أداة).

while True:
    ## user input
    try:
        q = input('🙂 >')
    except EOFError:
        break
    if q == "quit":
        break
    if q.strip() == "":
        continue
    messages.append( {"role":"user", "content":q} )
   
    ## model
    agent_res = ollama.chat(
        model=llm,
        tools=[tool_search_web, tool_search_yf],
        messages=messages)

حتى هذه النقطة، قد يبدو سجل الدردشة شيئًا مثل هذا:

إذا كان النموذج يريد استخدام أداة، فيجب تشغيل الدالة المناسبة باستخدام معلمات الإدخال المقترحة بواسطة LLM في كائن الاستجابة الخاص به:

لذلك يحتاج الكود الخاص بنا إلى الحصول على هذه المعلومات وتشغيل دالة الأداة.

## response
    dic_tools = {'search_web':search_web, 'search_yf':search_yf}

    if "tool_calls" in agent_res["message"].keys():
        for tool in agent_res["message"]["tool_calls"]:
            t_name, t_inputs = tool["function"]["name"], tool["function"]["arguments"]
            if f := dic_tools.get(t_name):
                ### calling tool
                print('🔧 >', f"\x1b[1;31m{t_name} -> Inputs: {t_inputs}\x1b[0m")
                messages.append( {"role":"user", "content":"use tool '"+t_name+"' with inputs: "+str(t_inputs)} )
                ### tool output
                t_output = f(**tool["function"]["arguments"])
                print(t_output)
                ### final res
                p = f'''Summarize this to answer user question, be as concise as possible: {t_output}'''
                res = ollama.generate(model=llm, prompt=q+". "+p)["response"]
            else:
                print('🤬 >', f"\x1b[1;31m{t_name} -> NotFound\x1b[0m")
 
    if agent_res['message']['content'] != '':
        res = agent_res["message"]["content"]
     
    print("👽 >", f"\x1b[1;30m{res}\x1b[0m")
    messages.append( {"role":"assistant", "content":res} )

الآن، إذا قمنا بتشغيل الكود الكامل، يمكننا الدردشة مع وكيلنا.

الوكيل المتقدم

يتقن LLM البرمجة من خلال التعرض لمجموعة كبيرة من الشيفرات البرمجية ونصوص اللغة الطبيعية، حيث يتعلم أنماط لغات البرمجة وقواعدها النحوية ودلالاتها. يتعلم النموذج العلاقات بين أجزاء الشيفرات البرمجية المختلفة من خلال توقع الرمز التالي في التسلسل. باختصار، يمكن ل LLM إنشاء شيفرة بايثون، لكن لا يمكنه تنفيذها، بينما يمكن للوكلاء تنفيذها.

سأُعِدّ أداةً تُمكّن العميل من تنفيذ الشيفرة. في بايثون، يُمكنك بسهولة إنشاء غلاف لتشغيل الشيفرة كسلسلة نصية باستخدام الأمر الأصلي()exec.

import io
import contextlib

def code_exec(code: str) -> str:\
    output = io.StringIO()
    with contextlib.redirect_stdout(output):
        try:
            exec(code)
        except Exception as e:
            print(f"Error: {e}")
    return output.getvalue()

tool_code_exec = {'type':'function', 'function':{
  'name': 'code_exec',
  'description': 'execute python code',
  'parameters': {'type': 'object',
                'required': ['code'],
                'properties': {
                    'code': {'type':'str', 'description':'code to execute'},
}}}}

## test
code_exec("a=1+1; print(a)")

تمامًا كما في السابق، سأكتب موجهًا، ولكن هذه المرة، في بداية حلقة الدردشة، سأطلب من المستخدم تقديم مسار الملف.

prompt = '''You are an expert data scientist, and you have tools to execute python code.
First of all, execute the following code exactly as it is: 'df=pd.read_csv(path); print(df.head())'
If you create a plot, ALWAYS add 'plt.show()' at the end.
'''
messages = [{"role":"system", "content":prompt}]
start = True

while True:
    ## user input
    try:
        if start is True:
            path = input('📁 Provide a CSV path >')
            q = "path = "+path
        else:
            q = input('🙂 >')
    except EOFError:
        break
    if q == "quit":
        break
    if q.strip() == "":
        continue
   
    messages.append( {"role":"user", "content":q} )

بما أن مهام الكود قد تكون أكثر صعوبةً على LLM، فسأضيف أيضًا تعزيزًا للذاكرة. افتراضيًا، لا توجد ذاكرة طويلة المدى حقيقية خلال الجلسة الواحدة. يستطيع LLM الوصول إلى سجل المحادثة، ما يسمح لهم بتذكر المعلومات مؤقتًا، وتتبع السياق والتعليمات التي قدمتها في بداية المحادثة. مع ذلك، لا تعمل الذاكرة دائمًا كما هو متوقع، خاصةً إذا كان مستوى LLM صغيرًا. لذلك، من الممارسات الجيدة تعزيز ذاكرة النموذج بإضافة تذكيرات دورية في سجل المحادثة.

prompt = '''You are an expert data scientist, and you have tools to execute python code.
First of all, execute the following code exactly as it is: 'df=pd.read_csv(path); print(df.head())'
If you create a plot, ALWAYS add 'plt.show()' at the end.
'''
messages = [{"role":"system", "content":prompt}]
memory = '''Use the dataframe 'df'.'''
start = True

while True:
    ## user input
    try:
        if start is True:
            path = input('📁 Provide a CSV path >')
            q = "path = "+path
        else:
            q = input('🙂 >')
    except EOFError:
        break
    if q == "quit":
        break
    if q.strip() == "":
        continue
   
    ## memory
    if start is False:
        q = memory+"\n"+q
    messages.append( {"role":"user", "content":q} )

يرجى ملاحظة أن طول الذاكرة الافتراضي في Ollama هو ٢٠٤٨ حرفًا. إذا كان جهازك قادرًا على استيعابه، يمكنك زيادته بتغيير الرقم عند استدعاء LLM:

## model
    agent_res = ollama.chat(
        model=llm,
        tools=[tool_code_exec],
        options={"num_ctx":2048},
        messages=messages)

في حالة الاستخدام هذه، يكون مخرجات الوكيل عبارة عن كود وبيانات في الغالب، لذا لا أريد أن يقوم LLM بإعادة صياغة الاستجابات.

## response
    dic_tools = {'code_exec':code_exec}
   
    if "tool_calls" in agent_res["message"].keys():
        for tool in agent_res["message"]["tool_calls"]:
            t_name, t_inputs = tool["function"]["name"], tool["function"]["arguments"]
            if f := dic_tools.get(t_name):
                ### calling tool
                print('🔧 >', f"\x1b[1;31m{t_name} -> Inputs: {t_inputs}\x1b[0m")
                messages.append( {"role":"user", "content":"use tool '"+t_name+"' with inputs: "+str(t_inputs)} )
                ### tool output
                t_output = f(**tool["function"]["arguments"])
                ### final res
                res = t_output
            else:
                print('🤬 >', f"\x1b[1;31m{t_name} -> NotFound\x1b[0m")
 
    if agent_res['message']['content'] != '':
        res = agent_res["message"]["content"]
     
    print("👽 >", f"\x1b[1;30m{res}\x1b[0m")
    messages.append( {"role":"assistant", "content":res} )
    start = False

الآن، إذا قمنا بتشغيل الكود الكامل، يمكننا الدردشة مع وكيلنا.

تناولت هذه المقالة الخطوات الأساسية لإنشاء وكلاء من الصفر باستخدام Ollama فقط. بهذه الخطوات الأساسية، ستكون جاهزًا للبدء بتطوير وكلاءك الخاصين لمختلف حالات الاستخدام.

ترقبوا الجزء الثاني، حيث سنتعمق أكثر في أمثلة أكثر تقدمًا.

أتمنى أن تكونوا قد استمتعتم بها! لا تترددوا في التواصل معي لأي استفسارات أو ملاحظات أو لمشاركة مشاريعكم الشيقة.


اكتشاف المزيد من بايثون العربي

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

Scroll to Top

اكتشاف المزيد من بايثون العربي

اشترك الآن للاستمرار في القراءة والحصول على حق الوصول إلى الأرشيف الكامل.

Continue reading