סיכום וובינר סוכנים חכמים
אתמול עשינו וובינר על סוכנים חכמים שהלך לדעתי הרבה יותר טוב מזה של שבוע שעבר. אם הייתם אני מקווה שגם לכם היה מעניין. המעבר לטימס כלל קצת קשיי הסתגלות ודי פספסתי את הצ'אט אבל אולי זה עודד אנשים להשתתף.
בכל מקרה אני רוצה לסכם כאן את עיקרי הדברים ולצרף את דוגמת הקוד שכתבנו.
1. מה זה בכלל סוכן
התחלנו את המפגש בשאלה הכי חשובה: מה זה בכלל סוכן. עבור השיחה שלנו הסכמנו שסוכן הוא תוכנת מחשב שכוללת גם קריאה למודל שפה. זאת לא הגדרה מושלמת ולא היחידה בתעשייה אבל היא עוזרת לנו להתקדם.
התובנה המשמעותית כאן היתה שסוכן לא חייב להיות תוכנה בממשק של Chat. גם תוכנה שמתייקת אימיילים יכולה להיקרא סוכן, גם משחק אסטרטגיה ששחקן המחשב הוא בעצם מודל שפה הוא סוכן, גם מערכת שמשאירה Code Reviews על קוד היא סוכן.
ואם סוכן זה כל מה שיש לו ממשק עם LLM אז פריימוורק לפיתוח סוכנים זו ספריה שמנסה לענות על אתגרים נפוצים ומשותפים בפיתוח תוכנות מסוג זה.
2. קוד או GUI, ואיזו פריימוורק
מאחר וסוכן הוא כל תוכנה שיש לה חיבור ל LLM ומאחר ואנחנו כל כך אוהבים לדבר עם מודלי שפה סיכוי טוב שחלק גדול מהתוכנות החדשות או מהרכיבים החדשים שייכנסו לתוכנות יהיו סוכנים.
יש כלים גרפיים רבים לפיתוח סוכנים בממשקי Drag & Drop. בין המפורסמים n8n ו opal. אם כלים כאלה פותרים לכם את הבעיה שווה להשתמש בהם.
אם אתם כמוני מעדיפים לראות קוד, או אם אתם צריכים לשלב סוכן במערכת קיימת, או אם אתם רוצים יותר שליטה על התוצאה כנראה שתרצו לבחור באפשרות של פיתוח קוד.
אפילו אחרי שבחרתם לכתוב את הסוכן שלכם בקוד יש שתי החלטות חשובות לקבל:
- באיזה שפה ופריימוורק להשתמש?
- איפה להריץ את הקוד?
שתי השאלות קשורות. אם בחרתי להריץ את הסוכן על AWS אז יש סט שפות ופריימוורקים שנתמכים. על Azure יש תמיכה בשפות אחרות. על Vercel שוב אחרות. אני תמיד יכול להריץ את הסוכן על שרת שלי ואז יש לי את הגמישות המירבית בבחירת השפות אבל אולי לא אקבל את השירותים שאני רוצה מהפלטפורמות ואצטרך לכתוב ולתחזק יותר קוד בצד שלי.
סביבות ההרצה עצמן והפריימוורקים נמצאים היום בתחרות על פיצ'רים ועל נתח שוק וכך אנחנו רואים את אותה בעיה מקבלת מענה בכמה שכבות. לדוגמה כולם יודעים שבשיחה עם סוכן חכם הרבה פעמים אנחנו רוצים לצרף להודעה גם את ההודעות הישנות ואת התשובות שלהן (כדי לתת הרגשה של שיחה רציפה). ספריית קוד כמו OpenAI Agents SDK כוללת מנגנון מובנה לניהול Sessions. אבל גם ה API של OpenAI בצד השרת יודע לשמור Sessions וגם סביבת הריצה Agent Core של AWS יודעת לנהל Session. כפילות זו תלווה אותנו בתקופה הקרובה עד שכלי הפיתוח ושיטות העבודה יתייצבו.
3. ארכיטקטורה בגדול
סוכן חכם שעובד מורכב ממספר שכבות:
שכבה ראשונה היא הטריגר - משהו צריך להפעיל תהליך. זה יכול להיות בקשת HTTP, שעה מסוימת (משימה מתוזמנת), אימייל נכנס, הודעה בטלגרם. הטריגר עובר למערכת שלנו מתוך סביבת הריצה ומוגדר בסביבת הריצה של הסוכן. אם הפעלתי את הסוכן שלי על AWS אז AWS יגדירו איזה טריגרים הם מכירים. אם אני מעלה את הסוכן ל Vercel אז הטריגר יגיע משם.
הטריגר מגיע לטיפול הקוד שלנו שרץ בתוך הפלטפורמה. אם אנחנו רצים על AWS אז רכיב של AWS שנקרא Agent Core מריץ את הקוד. אם זה על Vercel אז הסוכן רץ בתור Vercel Function. בכל מקרה בקוד שלנו נראה נקודת כניסה לפונקציית ההתחלה של הסוכן שיכולה לקבל מידע מהטריגר.
הקוד שלנו צריך להבין מתוך המידע שהגיע מהטריגר מי המשתמש ומה הוא רוצה. אחרי שהבנו את זה אפשר לטעון הודעות קודמות מבסיס הנתונים ולבנות פניה או מספר פניות ל LLM.
אחרי שסיימנו לטפל באירוע אנחנו צריכים להחליט מה עושים עם התוצאה. לפעמים נרצה לשלוח את התוצאה ישירות למי שהפעיל את הסוכן, למשל אם זה הודעה שהגיעה ב HTTP. לפעמים נרצה לבצע פעולה ברקע כמו שליחת אימייל או השארת Code Review בגיטהאב.
4. קוד של סוכן שאוסף מידע מאתרי חדשות
אחרי שהבנו איך עובד סוכן המשכנו לבנות סוכן לדוגמה בעזרת הספריה OpenAI Agents SDK. בעולם היום ממילא קלוד כותב את הקוד ולכן כתבנו את הפרומפט הבא:
Bootstrap the agent according to Claude.md
in single file main.py
Use Gemini and the OpenAI compatible chat endpoint
Print out the daily digest to stdout
Tech sites should include https://www.reddit.com/r/programming/, hackernews and whatever else you can think of
והקוד שהוא יצר בקובץ אחד בשם main.py היה:
"""News Searcher: searches tech news sites and prints a daily digest to stdout.
Built with the OpenAI Agents SDK, running Gemini through its
OpenAI-compatible chat completions endpoint.
"""
import asyncio
import datetime
import os
import httpx
from agents import (
Agent,
OpenAIChatCompletionsModel,
Runner,
function_tool,
set_tracing_disabled,
)
from dotenv import load_dotenv
from openai import AsyncOpenAI
load_dotenv()
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
MODEL_NAME = "gemini-2.5-flash"
gemini_client = AsyncOpenAI(
base_url=GEMINI_BASE_URL,
api_key=os.environ["GEMINI_API_KEY"],
)
# Tracing exports to the OpenAI platform, which we have no key for.
set_tracing_disabled(True)
HTTP_HEADERS = {"User-Agent": "news-digest-agent/0.1 (daily tech digest demo)"}
async def _get_json(url: str, params: dict | None = None):
async with httpx.AsyncClient(
headers=HTTP_HEADERS, timeout=30, follow_redirects=True
) as client:
response = await client.get(url, params=params)
response.raise_for_status()
return response.json()
@function_tool
async def fetch_hacker_news(limit: int = 15) -> list[dict]:
"""Fetch the current Hacker News front page stories.
Args:
limit: Maximum number of stories to return.
"""
data = await _get_json(
"https://hn.algolia.com/api/v1/search",
params={"tags": "front_page", "hitsPerPage": limit},
)
return [
{
"title": hit["title"],
"url": hit.get("url"),
"points": hit.get("points"),
"comments": hit.get("num_comments"),
"discussion": f"https://news.ycombinator.com/item?id={hit['objectID']}",
}
for hit in data["hits"]
]
@function_tool
async def fetch_reddit_programming(limit: int = 15) -> list[dict]:
"""Fetch the hottest posts from reddit.com/r/programming.
Args:
limit: Maximum number of posts to return.
"""
data = await _get_json(
"https://www.reddit.com/r/programming/hot.json", params={"limit": limit}
)
return [
{
"title": post["data"]["title"],
"url": post["data"]["url"],
"score": post["data"]["score"],
"comments": post["data"]["num_comments"],
"discussion": f"https://www.reddit.com{post['data']['permalink']}",
}
for post in data["data"]["children"]
if not post["data"].get("stickied")
]
@function_tool
async def fetch_lobsters(limit: int = 15) -> list[dict]:
"""Fetch the hottest stories from lobste.rs.
Args:
limit: Maximum number of stories to return.
"""
data = await _get_json("https://lobste.rs/hottest.json")
return [
{
"title": story["title"],
"url": story["url"] or story["comments_url"],
"score": story["score"],
"comments": story["comment_count"],
"tags": story["tags"],
"discussion": story["comments_url"],
}
for story in data[:limit]
]
@function_tool
async def fetch_devto(limit: int = 15) -> list[dict]:
"""Fetch today's top articles from dev.to.
Args:
limit: Maximum number of articles to return.
"""
data = await _get_json(
"https://dev.to/api/articles", params={"top": 1, "per_page": limit}
)
return [
{
"title": article["title"],
"url": article["url"],
"reactions": article["positive_reactions_count"],
"comments": article["comments_count"],
"tags": article.get("tag_list", []),
}
for article in data
]
news_agent = Agent(
name="News Searcher",
instructions=(
"You are a tech news editor producing a daily digest.\n"
"Use ALL of your tools to gather today's top stories from Hacker News, "
"r/programming, Lobsters, and Dev.to.\n"
"Then write a digest in markdown with:\n"
"- A title with today's date and a 2-3 sentence overview of the day's "
"main themes.\n"
"- A 'Top Stories' section: the 5-8 most significant stories across all "
"sources, each with a one-sentence summary based on its title, a link, "
"and its source.\n"
"- A short 'Also Worth a Look' bullet list of 5-10 more headlines with "
"links.\n"
"Merge duplicate stories that appear on multiple sources and mention "
"each source's engagement (points/score/comments) where notable."
),
model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=gemini_client),
tools=[fetch_hacker_news, fetch_reddit_programming, fetch_lobsters, fetch_devto],
)
async def main() -> None:
today = datetime.date.today().strftime("%B %d, %Y")
result = await Runner.run(
news_agent, f"Generate the tech news digest for today, {today}."
)
print(result.final_output)
if __name__ == "__main__":
asyncio.run(main())
כמה דברים ששמתי לב מקריאת הקוד שלא הספקתי להציג בוובינר:
משיכת המידע מאתרי חדשות היא באמצעות פניה לכל API של אתר חדשות, ושונה לכל אתר. לכל אתר חדשות פונקציה משלו. אופציה אחרת שלדעתי היתה עובדת טוב יותר היא להגדיר רשימה של אתרי חדשות, למשוך את ה HTML-ים שלהם ולהשתמש בספריה כמו Readability כדי להגיע לתוכן של כל אתר, ואת זה פשוט לתת ל LLM. אני מבין למה קלוד השתמש ב APIs של כל אתר אבל נראה לי שזה יותר קשה לתחזוקה ושינוי. במיוחד אם רוצים לדמיין עולם בו משתמשים שונים יקבלו סקירה של אתרים שונים שמעניינים אותם.
הסיכום עצמו הוא פשוט התשובה של המודל. זה שוב נוח אבל לא תמיד עקבי. יותר הגיוני במערכת כזו להגדיר לסוכן להחזיר מידע גולמי ולהכניס אותו לתוך תבנית שהכנתם מראש.
הסוכן שכתבנו פשוט מדפיס את התשובה למסך. יותר הגיוני לשלוח את התשובה הזאת למייל שמוגדר מראש במערכת או למייל שהתקבל יחד עם הבקשה כפרמטר.
הסוכן לא יכול לקרוא את המאמרים עצמם ולכן יכול להתבסס רק על הכותרות, מספר התגובות והתיוגים של כל מאמר. סוכן יותר מתוחכם היה משתמש בכלי למשיכת מאמרים כדי לקרוא את המאמרים לפני שממליץ עליהם.
סך הכל מדובר בסוכן פשוט שעובד בדיוק לפי הפרומפט שהוגדר. היכולת היחידה של OpenAI Agents SDK המעורבת כאן היא Tool Call, כלומר היכולת של המודל "לקרוא" חזרה לקוד פייתון כדי למשוך מאמרי חדשות מהאתרים.
המימוש באמצעות סוכן קידוד מזכיר לנו את הכח של סוכני קידוד ואת התפקיד שלנו כמפתחים בעולם העתידי של קידוד עם סוכנים. הסוכן יודע לקחת רעיון ולהפוך אותו לקוד. המשימה שלנו היא לעלות ברמת האבסטרקציה ולהבין איך אנחנו רוצים שהמערכת תעבוד.