יום 18 - תבנית LLM בתור שופט
אנחנו לקראת סיום הסידרה ואני רוצה לדבר על כמה תבניות של סוכני AI שאנחנו רואים בתוכניות אמיתיות. הדוגמה הראשונה נקראת LLM בתור שופט והיא נועדה להתמודד עם חוסר הקונסיסטנטיות של מודלי שפה. בתבנית זו יש לנו משימה לבצע שאנחנו חושבים שמודל שפה יכול לבצע די טוב, אבל אנחנו יודעים שאי אפשר לסמוך על מודלי שפה ושלפעמים הם צריכים כמה ניסיונות כדי לייצר תוצאה טובה, לכן אנחנו נוסיף מודל נוסף שיבדוק את העבודה של המודל הראשון. רק כשהמודל השני, השופט, יאשר, נוכל להתקדם עם התוצאה.
1. קוד הדוגמה
זה הקוד המלא, מבוסס על הדוגמה של OpenAI מכאן:
https://github.com/openai/openai-agents-python/blob/main/examples/agentpatterns/llmasajudge.py
import asyncio
from dataclasses import dataclass
from typing import Literal
from agents import Agent, Runner, trace, SQLiteSession
"""
This example shows the LLM as a judge pattern. The first agent generates an outline for a story.
The second agent judges the outline and provides feedback. We loop until the judge is satisfied
with the outline.
"""
story_outline_generator = Agent(
name="story_outline_generator",
instructions=(
"You generate a very short story outline based on the user's input. "
"If there is any feedback provided, use it to improve the outline."
),
)
@dataclass
class EvaluationFeedback:
feedback: str
score: Literal["pass", "needs_improvement", "fail"]
evaluator = Agent(
name="evaluator",
instructions=(
"You evaluate a story outline and decide if it's good enough. "
"If it's not good enough, you provide feedback on what needs to be improved. "
"Never give it a pass on the first try. After 5 attempts, you can give it a pass if the story outline is good enough - do not go for perfection"
),
output_type=EvaluationFeedback,
)
async def main() -> None:
writer_session = SQLiteSession("story")
judge_session = SQLiteSession("judge")
msg = input("What kind of story would you like to hear? ")
latest_outline: str | None = None
# We'll run the entire workflow in a single trace
with trace("LLM as a judge"):
while True:
story_outline_result = await Runner.run(
story_outline_generator,
msg,
session=writer_session
)
latest_outline = story_outline_result.final_output
print("Story outline generated")
evaluator_result = await Runner.run(evaluator, msg, session=judge_session)
result: EvaluationFeedback = evaluator_result.final_output
print(f"Evaluator score: {result.score}")
if result.score == "pass":
print("Story outline is good enough, exiting.")
break
print("Re-running with feedback")
await writer_session.add_items([{"content": f"Feedback: {result.feedback}", "role": "user"}])
print(f"Final story outline: {latest_outline}")
if __name__ == "__main__":
asyncio.run(main())
2. הגדרת הסוכנים
התוכנית משתמשת בשני סוכנים - סוכן אחד כותב סיפורים וסוכן שני מייצר פידבק כדי לשפר את הסיפורים. נשים לב שכשהסוכן שנותן פידבק מקבל את הסיפור הוא לא יודע אפילו איזה סוג פידבק לתת, בדוגמה כאן המטרה שלו רק לשפר את התוצאות ולהגיע לתוצאה יותר קונסיסטנטית. לכן הבקשה בפרומפט לסרב לסיפורים הראשונים שהוא מקבל ורק אחרי 5 סיפורים ואם הסיפור טוב מספיק אז לאשר. זה הקוד שמגדיר את שני הסוכנים:
story_outline_generator = Agent(
name="story_outline_generator",
instructions=(
"You generate a very short story outline based on the user's input. "
"If there is any feedback provided, use it to improve the outline."
),
)
@dataclass
class EvaluationFeedback:
feedback: str
score: Literal["pass", "needs_improvement", "fail"]
evaluator = Agent(
name="evaluator",
instructions=(
"You evaluate a story outline and decide if it's good enough. "
"If it's not good enough, you provide feedback on what needs to be improved. "
"Never give it a pass on the first try. After 5 attempts, you can give it a pass if the story outline is good enough - do not go for perfection"
),
output_type=EvaluationFeedback,
)
הפידבק של הסוכן מועבר לתוך Structured Output בקלאס EvaluationFeedback. שימו לב שאנחנו מקבלים גם תוצאה score בה נשתמש כדי להבין מה לעשות עם הסיפור וגם הסבר feedback. את הפידבק אנחנו מעבירים לסוכן כותב הסיפורים כדי לשפר את הסיפור ובאופן כללי תמיד כשמקבלים Structured Output כדאי להוסיף שדה טקסט חופשי כדי שהמודל יוכל להתבטא (כן זה עוזר לקבל תוצאה טובה יותר).
3. זרימת התוכנית
מבחינת זרימת התוכנית עצמה אני מחזיק שתי Sessions - אחת עבור היסטוריית ההודעות של סוכן כותב הסיפורים, כדי שהוא יוכל בכל הודעה לראות את כל הגירסאות הקודמות של הסיפור והפידבקים הקודמים שקיבל. ה Session השני הוא של evaluator כדי שהוא יוכל לשפר את הפידבק ולראות את השיפור בסיפור לפי הפידבק שהוא מעביר. בשביל להעביר הודעה בין שתי הרשימות אני משתמש בפקודה:
await writer_session.add_items([{"content": f"Feedback: {result.feedback}", "role": "user"}])
התוכנית רצה בלולאה עד שה evaluator מחזיר שהסיפור מספיק טוב עם הקוד הזה:
if result.score == "pass":
print("Story outline is good enough, exiting.")
break
4. עכשיו אתם
הריצו את התוכנית אצלכם וכנסו למסך הלוג ב OpenAI כדי לראות את כל ההודעות שעוברות לשני הסוכנים. לאחר מכן שמרו את שתי ה Sessions לקבצי sql והתבוננו בהודעות שנשמרו אצלכם על המכונה. חישבו: באיזה מצבים נרצה שה Evaluator ירוץ באותו Session? מתי עדיף להפריד ולהעתיק הודעות בצורה ידנית?
לסיום חשבו - מה אם ה evaluator אף פעם לא יהיה מרוצה? עדכנו את הקוד כך שבכל מקרה אחרי 7 ניסיונות הלולאה תיפסק. אם ה evaluator לא היה מרוצה הציגו למשתמש הודעה שלא הצלחנו ליצור סיפור מספיק טוב.