בעוד הכותרות בעיתונים מודיעות שתם עידן ואין שום טעם לעשות שום דבר למעט לחכות לאבטלה ולפנסיה המבורכת, יש על השולחן שלי לא מעט מטלות הקשורות לבניית מוצרים של LLM. בפוסט הזה אני אכנס לעומק קצת יותר על אייג׳נטים (סוכנים) וכלים כדי לעשות קצת די מיסטיפקציה ולהבין מה זה אייג׳נטים. כשהתחלתי לעבוד עם אייג׳נטים לפני… ואוו בעצם כבר כמה שנים (!!!), הבנתי שבסופו של דבר השם מאד מפוצץ אבל בפועל מדובר במשהו מאד פשוט LLM איטרטיבי שיכול להשתמש בכלים.
האיטרציה
הלב של כל איג׳נט הוא איטרציה. כלומר לולאה. ה-LLM מפעיל או חושב על משהו ואז חוזר לעצמו עם התשובה. בכל סביבוב של הלולאה האייג׳נט מעמיס עוד ועוד ועוד על הקונטקסט ונותן עוד מידע לעצמו. כמו נחש שאוכל את עצמו שוב ושוב ושוב. אבל בכל סיבוב יש לו יותר ויותר מידע עד שהוא מחזיר את התשובה.
בואו ננסה להבין את זה תיאורטית. נניח ויש לי סוכן. אני שואל אותו ״אני רוצה לשפר את הכושר הגופני שלי ולרוץ, בנה לי תוכנית.״
LLM קלאסי פשוט יתן תשובה. אבל סוכן לעומת זאת ירוץ בלולאה. צעד ראשון הוא ״אסוף מידע״. ה-LLM ישאל את המשתמש: ״מה הכושר הנוכחי שלך?״.
המשתמש יענה לו ״אני רץ 3 פעמים בשבוע, 10 ק״מ בקצב של 6:15 דקות לקילומטר״.
הסקריפט, שהוא סקריפט פייתוני פשוט, לוקח את השאלה המקורית של המשתמש, לוקח את השאלה של המודל, את התשובה של המשתמש ומזין אותם שוב ל-LLM. הצעד השני הוא ״וודא שאתה לא עושה נזק״. ה-LLM יחשוב ויאמר ״בשביל לבדוק שאנחנו לא עושים נזק, נשאל את המשתמש האם יש לו בעיות בריאות קיימות״. המשתמש יענה: ״אין לי״. עכשיו כל הנתונים האלו שכוללים:
- השאלה המקורית של המשתמש.
- השאלה שה-LLM שואל את עצמו ״אסוף מידע״.
- התשובה של המשתמש עם ההסבר על המידע.
- השאלה הנוספת של ה-LLM על ״האם יש בעיות בריאות״.
- התשובה של המשתמש: ״אין לי בעיות״.
- המטרה המקורית: לבנות תוכנית.
עם כל הקונטקסט הזה ה-LLM יספק תשובה מלאה וטובה יותר. זה קורה הרבה פעמים באופן אוטומטי בצ׳אט וזה נקרא ״חשיבה פנימית״. מה זו חשיבה פנימית? איטרציות. זה הכל. ברגע שאתם מבינים את האיטרציה, הבנתם איך אייג׳נטים עובדים.
לעתים האיטרציה הזו מאד מובנת. אם אתם בונים סוכן שיש לו מטרה מסוימת, האיטרציה היא קשוחה מאד ולא ניתן חופש ל-LLM אלא נלך לפי צעדים קשיחים יותר. לפעמים אנחנו נצטרך לתת יותר חופש. השאלה היא שוב המטרה.
העניין מתחיל להסתבך כאשר אנחנו נותנים ל-LLM גישה לכלים.
כלים
כלים הם פרוטוקול פשוט. יחד עם הקונטקסט של השאלות לאייג׳נט, אנחנו גם מעבירים רשימה של פונקציות עם ארגומנטים שה-LLM יכול להפעיל לבד. זה נשמע מסובך ומורכב אבל זה חלק מה-API של כמעט כל LLM. מעבירים את רשימת הכלים בבקשת ה-API. למשל, הנה הדוקומנטציה של גרוק, ה-LLM החביב עלי שעוסקת בכלים. בגדול זה די קל. אני מעביר כחלק מה-API רשימה של כלים עם תקציר עליהם. למשל:
- הכלי get_salaries מחזיר את המשכורות של העובדים, הוא מקבל ארגומנט של מספר עובד.
- הכלי get_employee מחזיר את אובייקט העובד (שם מלא, מספר עובד וכתובת) ומקבל ארגומנט של שם העובד.
אני מעביר את הרשימה והתקציר יחד עם השאילתה שלי. ה-LLM מחזיר תשובה ומבקש ממני להפעיל את הכלי ולתת לו את התוצאה, אני מחזיר את התוצאה והוא, כשיש לו את כל הקונטקסט והתוצאה של הכלי מחזיר את התשובה. בואו ונראה איך זה עובד. אני נותן פקודה לסוכן: ״תגיד לי מה המשכורת של משה כהן?״. השאילתה הזו נכנסת ל-LLM יחד עם רשימת הכלים. ה-LLM אומר: ״בשביל לדעת מה המשכורת של משה כהן, אני צריך לדעת מה מספר העובד שלו, את זה אני יכול לעשות עם get_employee״ ומחזיר תשובה: ״תפעיל את get_employee יחד עם הארגומנט ״משה כהן״״. אני מקבל את התשובה, נניח 678 ומפעיל סקריפט בפייתון שקורא לכלי ומשגר ל-LLM בקשה נוספת עם התוצאה מהכלי והקונטקסט המלא. ה-LLM חושב שוב: ״טוב, יש לי את מספר העובד של משה כהן, אני צריך להשתמש ב-get_salaries עם הארגומנט של מספר העובד״ והתוצאה היא בקשה של ״תפעיל את כלי get_salaries עם מספר העובד 678״. אני מקבל את התשובה הזו, מפעיל את הכלי ומחזיר את התוצאה ל-LLM יחד עם הקונטקסט. בשלב האחרון ה-LLM ישתמש בתוצאה הזו כדי להחזיר תשובה.
ואז כולם צורחים ש״סוף האנושות הגיע!״ או משהו בסגנון ומתפעלים מחוכמתו וכוחו האדיר של הסוכן, חוץ מאלו שיודעים איך הוא עובד. עכשיו זה אתם.
הדגמה חיה
אני מת על לראות דברים ממש בעיניים. אז בואו וניצור אייג׳נט בפייתון. זה אייג׳נט מאד טיפש ובסיסי, אבל אחד שבהחלט יכול להדגים. בניגוד לאייג׳נט רגיל, בכל איטרציה תהיה עצירה ונוכל לראות ממש את זה בעיניים. אז אני יוצר מסד נתונים עם sqlite שיאוכלס בנתוני דמה של חנות ספרים. זה השלב הראשון בקוד.
השלב השני הוא ליצור פונקציות שמשתמשות במסד הנתונים. הראשונה היא get_top_selling_books והשניה היא get_book_price.
שני השלבים ממש כתובים בקוד והם setup. קוד פייתוני פשוט (גם מי שמכיר רק ג׳אווהסקריפט לא יתקשה להבין, זה לא מדע טילים שם). מה שרלוונטי הוא החלק השלישי. השתמשתי בגרוק, אבל כל LLM יכול לעבוד. מה עשיתי? שלחתי ל-API את השאלה – מה המחיר של הספר שמכר הכי הרבה בשנת 2023 ושלחתי את רשימת הכלים. בנוסף, בתשובה בדקתי אם יש בקשה להפעלת כלים. אם יש – הפעלתי ושלחתי את התשובה. לולאה הכי פשוטה שיש שלא נגמרת כל עוד גרוק רוצה להפעיל כלים. סיים? זו התשובה הסופית שלו. זהו.
אז ככה נראה הקוד, מה שרלוונטי עבורכם זה החלק השלישי:
import os
import sqlite3
import random
import json
from openai import OpenAI
# ==========================================
# 1. Set up the database and populate book data
# ==========================================
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE book_prices (
book_title TEXT,
year INTEGER,
price REAL
)
""")
cursor.execute("""
CREATE TABLE book_sales (
book_title TEXT,
year INTEGER,
total_sales INTEGER
)
""")
books = ["Learn Python", "Learn JavaScript", "The Professional MySQL Guide"]
# Generate consistent random data for the years 2020 to 2025
random.seed(42)
for book in books:
for year in range(2020, 2026):
price = round(random.uniform(120, 180), 2)
monthly_sales = random.randint(20, 100)
yearly_sales = monthly_sales * 12
cursor.execute("INSERT INTO book_prices VALUES (?, ?, ?)", (book, year, price))
cursor.execute("INSERT INTO book_sales VALUES (?, ?, ?)", (book, year, yearly_sales))
conn.commit()
# ==========================================
# 2. Define the tools for the agent
# ==========================================
def get_top_selling_books(year: int) -> str:
"""Returns the top selling books ordered by total sales volume for a specific year."""
print(f"[SQL LOG] Running query: SELECT book_title FROM book_sales WHERE year = {year}...")
cursor = conn.cursor()
cursor.execute(
"SELECT book_title, total_sales FROM book_sales WHERE year = ? ORDER BY total_sales DESC LIMIT 10",
(year,)
)
results = cursor.fetchall()
if not results:
return f"No sales data found for the year {year}."
return ", ".join([f"'{row[0]}' ({row[1]} copies)" for row in results])
def get_book_price(book_title: str, year: int) -> str:
"""Returns the price of a specific book for a given year."""
print(f"[SQL LOG] Running query: SELECT price FROM book_prices WHERE book_title LIKE '%{book_title}%' AND year = {year}...")
cursor = conn.cursor()
cursor.execute(
"SELECT price FROM book_prices WHERE book_title LIKE ? AND year = ?",
(f"%{book_title}%", year)
)
result = cursor.fetchone()
if not result:
return f"No price data found for '{book_title}' in year {year}."
return f"The price of the book '{book_title}' in {year} was {result[0]} ILS."
# Define the schema Grok needs in order to know which tools are available
# זה החלק הכי חשוב
tools_definition = [
{
"type": "function",
"function": {
"name": "get_top_selling_books",
"description": "Returns the top selling books ordered by total sales volume for a specific year.",
"parameters": {
"type": "object",
"properties": {
"year": {"type": "integer", "description": "The year to check sales for."}
},
"required": ["year"]
}
}
},
{
"type": "function",
"function": {
"name": "get_book_price",
"description": "Returns the price of a specific book for a given year.",
"parameters": {
"type": "object",
"properties": {
"book_title": {"type": "string", "description": "The title of the book."},
"year": {"type": "integer", "description": "The year to check the price for."}
},
"required": ["book_title", "year"]
}
}
}
]
# ==========================================
# 3. Initialize the client and the iteration loop (Grok)
# ==========================================
# Configure the client to work against the xAI API (assumes the XAI_API_KEY env var is set)
client = OpenAI(
api_key=os.environ.get("XAI_API_KEY"),
base_url="https://api.x.ai/v1"
)
prompt = "What was the price of the best-selling book in 2023?"
messages = [{"role": "user", "content": prompt}]
print(f"--- Starting task: {prompt} ---\n")
tools_map = {
"get_top_selling_books": get_top_selling_books,
"get_book_price": get_book_price
}
for iteration in range(1, 6):
print(f"[Iteration {iteration}] Sending the message history to Grok...")
# Call the Grok model
response = client.chat.completions.create(
model="grok-3",
messages=messages,
tools=tools_definition,
tool_choice="auto",
temperature=0
)
response_message = response.choices[0].message
# Add the model's response to the history (required for the next iteration)
messages.append(response_message)
# Check whether the model decided to call a tool
if response_message.tool_calls:
print(f"-> Grok decided it needs to call a tool (Tool Call).")
for tool_call in response_message.tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
print(f" [Model request] Asking to run: '{function_name}' with arguments: {function_args}")
# Run the local code
tool_func = tools_map.get(function_name)
if tool_func:
tool_result = tool_func(**function_args)
print(f" [Local result] The answer returned from the DB: \"{tool_result}\"\n")
# Return the result to Grok in the format the protocol requires
messages.append({
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": tool_result,
})
# Continue to the next iteration so Grok can read the data returned from the tool
continue
# If there are no tool_calls, Grok has finished thinking and reached a final answer
print(f"--- Final answer from Grok ---")
print(response_message.content)
break
conn.close()
והתוצאה היא ממש פשוטה. יש שלוש איטרציות. הראשונה – הפעלת הכלי של מה הספר הכי נמכר בשנת 2023. אני קורא לכלי, מחזיר את התשובה. הריצה השניה – קריאה נוספת של לבדוק מה המחיר של הספר. אני קורא לכלי ומחזיר. איטרציה שלישית – גרוק משתמש בנתונים כדי לתת את התשובה הסופית. בתשובה הסופית אין קריאה לכלים אז הקוד הפייתוני יודע שהוא סוגר את הלולאה ומחזיר את התשובה.
--- Starting task: What was the price of the best-selling book in 2023? ---
[Iteration 1] Sending the message history to Grok...
-> Grok decided it needs to call a tool (Tool Call).
[Model request] Asking to run: 'get_top_selling_books' with arguments: {'year': 2023}
[SQL LOG] Running query: SELECT book_title FROM book_sales WHERE year = 2023...
[Local result] The answer returned from the DB: "'Learn Python' (1068 copies), 'Learn JavaScript' (924 copies), 'The Professional MySQL Guide' (816 copies)"
[Iteration 2] Sending the message history to Grok...
-> Grok decided it needs to call a tool (Tool Call).
[Model request] Asking to run: 'get_book_price' with arguments: {'book_title': 'Learn Python', 'year': 2023}
[SQL LOG] Running query: SELECT price FROM book_prices WHERE book_title LIKE '%Learn Python%' AND year = 2023...
[Local result] The answer returned from the DB: "The price of the book 'Learn Python' in 2023 was 160.6 ILS."
[Iteration 3] Sending the message history to Grok...
--- Final answer from Grok ---
160.6 ILS (for "Learn Python")
ו… זהו! קוד פייתון פשוט (אפשר גם בטייפסקריפט ובעצם בכל שפה אחרת). אין פה קסם. תכנות, קריאה לכלים, אין וודו.
סיכום
זו כמובן דוגמה נאיבית מאד להבנה בלבד איך אייג׳נט עובד. ממש מאפס ואיך מממשים אחד כזה באופן הכי פשוט שיש. אבל בעולם המציאותי אנחנו לא בונים דברים כאלו מאפס כמובן אלא משתמשים בפריימוורקים שנותנים גמישות הרבה יותר גדולה במציאות מורכבת. אבל הצעד הראשון של הבנה מה זה וההבנה שמדובר בסך הכל ב-wrapper פשוט נעשתה. בפוסט הבא נדבר על פרויקט יותר מורכב.







6 תגובות
כמו תמיד תענוג לקרוא את התוכן שלך.😃
יש פוסטים שחייבים לראות באיזה סרטון כדי לקבל את התמונה המלאה אז אם יתאפשר לך זה יהיה מעולה.
בכל אופן תודה על עוד פוסט מצוין 🙏
אני אחשוב על זה, תודה רבה! 🙂
באילו פריימוורקים משתמשים ?
אני אכתוב על זה יותר בפירוט בפוסט הבא.
יכול להיות שפיספסתי משהו כאן, אבל איזה ערך מוסף כלי הLLM נתן כאן? אתה כתבת בעצמך שתי שאילתות SQL, ונתת לגרוק להריץ אותן. מה זה שונה אם היית מריץ את השאילתות בעצמך?
במקרה הזה מאד הנחיתי אותו והיו רק שני כלים והשתמשתי בשאילתה מאד מאד מובנית. אבל תחשוב בעולם שיש בו יותר מ-2 כלים. למשל 4. והשאלה של המשתמש היא קצת יותר מורכבת. אני לא יכול לפרק את זה לשלבים בלי ה-LLM. הוא זה מבצע את הפעולות וקובע את מספר האיטרציות.