הקדמה

מבט מלמעלה

  • פלטפורמת פיתוח קוד פתוח עבור יישומי LLM.
  • קיימות חבילות בפייתון ובJS(TypeScript).
  • מתמקדת בהרכבה ומודולריות. הערך המרכזי שהיא מביאה:
  1. רכיבים מודולריים.
  2. מקרים שימושיים (use cases): דרכים נפוצות לשלב רכיבים.

רכיבים (Modules)

הרכיבים המרכזיים של LangChain מתייחסים למודלים, פרומפטים, אינדקסים, שרשראות וסוכנים.

  • מודלים (Models)
    • יותר מ-20 אינטגרציות למודלי שפה גדולים (LLMs)
    • מודל שיחה (Chat models)
    • מודלי שיכון טקסט (Text embedding): 10+ אינטגרציות
  • פרומפטים (Prompts)
    • תבניות פרומפט (Prompt templates)
    • פרסור פלט (Output Parsers): 5+ יישומים
      • לוגיקה לניסיון חוזר/תיקון
    • בוררי דוגמאות (Example Selectors): 5+ יישומים
  • אינדקסים (Indexes)
    • טעינת מסמכים (Document Loaders): 50+ יישומים
    • פיצול טקסט (Text Splitters): 10+ יישומים
    • מאגרי וקטורים (Vector stores): 10+ אינטגרציות
    • מאחזרים (Retrievers): 5+ אינטגרציות/יישומים
  • שרשראות (Chains)
    • פרומפט + LLM + פרסור פלט
    • ניתן לשמש כבלוקי בנייה לשרשראות ארוכות יותר
    • עוד שרשראות ספציפיות לשימוש: 20+ סוגים
  • סוכנים (Agents)
    • סוגי סוכנים (Agent Types): 5+ סוגים
      • אלגוריתמים להפעלת LLMs באמצעות כלים
    • ערכות כלים לסוכנים (Agent Toolkits): 10+ יישומים
      • סוכנים עם כלים ספציפיים ליישום ספציפי

מודלים (Models), פרומפטים (Prompts) ומפרסרים (Parsers)

כשאנחנו מדברים על מודל (Model) אנחנו מתייחסים למודל השפה הגדול (LLM), כשאנחנו מדברים על פרומפט (Prompt) אנחנו מתייחסים לצורת היצירה של הקלט שאנחנו מעבירים למודל, ומפרסרים (Output parsers) מתייחס לדרך שבה אנחנו לוקחים את הפלט של המודל ומפרסרים אותו לצורה יותר מבנית (Structured). אחת התכונות המרכזיות של LangChain היא ההפשטה.

התקנה

לתחילת עבודה עם Langchain נבצע התקנה באמצעות:

!pip install --upgrade langchain

מודל

כדי לייצר מודל של OpenAI מסוג Chat, נשתמש ב:

from langchain.chat_models import ChatOpenAI

נשים לב שנאתחל מודל של OpenAI אין צורך לשלוח את משתני הסביבה הרלוונטיים, LangChain מחפשת אותם במידת הצורך:

chat = ChatOpenAI()
chat

התוצאה תביא מקבילה לget_complition מהקורסים הקודמים, עם מבנה מורכב יותר של ההודעות כלומר של הפרמטר messages, ובאופן כללי מבנה אחר של פורמט של LangChain. יהיה לנו נוח להשתמש בתבניות לפרומפטים. הערה: לא הרחבנו פה על השימוש ב()ChatOpenAI וגם ישנם שינוים בגרסאות הנוכחיות, פירוט והפניות בהערות.

תבנית פרומפטים

תבנית פרומפטים, כשמה, היא הפשטה של הפרומפט כך שייצר אובייקט של תבנית (מסוג PromptTemplate) שיוכל לקבל כקלט משתנים, וישמור את התבנית לשימושים חוזרים. הערכים החשובים באובייקט PromptTemplate הם:

  • משתני הקלט input_variables - המשתנים הנדרשים לאתחול התבנית.
  • תבנית template - הטקסט המקורי של התבנית. בקורס משתמשים בפונקציה שונה מהבנאי של PromptTemplate אלא ישר בChatPromptTemplate שהוא אובייקט של תבנית של צ’אט פרומפטים.

דוגמא

נאתחל תבנית בתצורה הבאה:

template_string = """... {var1} \
... {var2} ...
"""

נייצר תבנית של צ’אט של הודעת משתמש בודדת:

from langchain.prompts import ChatPromptTemplate
 
prompt_template = ChatPromptTemplate.from_template(template_string)

נוכל להסתכל על הטקסט של ההודעה לאחר איתחול המשתנים באמצעות גישה לתוכן להודעה במיקום ה0 (מפני שזו ההודעה היחידה בתבנית הצ’אט):

prompt_template.format_messages(var1='Some var1', var2='Some var2')[0].content

כעת נקרא למודל לאחר פרמוט ההודעות:

chat(prompt_template.format_messages(var1='Some var1', var2='Some var2'))

הצורך

תבנית פרומפטים היא לא דבר הכרחי אבל הסיבות הבאות הם מוטיבציה ללמה להשתמש בתבניות פרומפטים:

  • פרומפטים יכולים להיות ארוכים ומלאים בפרטים
  • ניתן להשתמש שוב (reuse) בפרומפטים טובים כשאפשר
  • ישנם פרומפטים נפוצים שLangChain מספקת
  • פרסור הפלט (שיוסבר בהמשך) משתמש בתבניות פרומפטים

פרסור פלט

במקרים רבים נרצה פלט בצורה מובנית. במקרים אלה נצטרך לבקש במפורש מהמודל את צורת הפלט, ובנוסף גם כשנקבל את הפלט מפורמפט (אם נקבל אותו מפורמט) נקבל אותו כמחרוזת כי זה הפלט של המודל, ולא תתבצע המרה לפורמט.

פרומפט בקשת הפורמט לפלט

בקורס הזה הביאו דוגמא למודלים פשוטים המשתמשים בעיקר בפורמפט חוזר מסוג JSON כדוגמא. לLangChain יש אפשרויות נוספות לפרסור פלט, אבל כאן נתמקד בצורה הפשוטה הזו.

מבנה שדה פלט

כדי להגדיר שדה פלט יחיד נשתמש בResponseSchema . הפרמטר name מגדיר את הkey של הפלט המבוקש, והפרמטרdecription מתאר תיאור של הנחיות של הערך המוחזר הנדרש.

הודעת בקשת הפלט המאוחד

כאשר יש לנו אוסף של שדות שהוגדרו, נצטרך לייצר הודעה מאוחדת שתשלח בסופה לפרומפט. נשתמש בStructuredOutputParser כדי לבצע זאת. ניתן לראות בדוגמא כיצד עושים זאת. בסוף התהליך הזה יהיה מחרוזת הנחיה למודל כיצד להחזיר את הפלט שלו אותה נכניס במשתנה בתבנית פרומפט.

דוגמא

from langchain.output_parsers import ResponseSchema
from langchain.output_parsers import StructuredOutputParser
 
element1_schema = ResponseSchema(name="key1_to_extract",
                             description="key1 extraction instruction.")
element2_schema = ResponseSchema(name="key2_to_extract",
                             description="key2 extraction instruction.")
 
response_schemas = [element1_schema, 
                    element2_schema]
                    
output_parser = StructuredOutputParser.from_response_schemas(response_schemas)        
format_instructions = output_parser.get_format_instructions()

התוצאה של זה תיהיה בערך:

The output should be a markdown code snippet formatted in the following schema, including the leading and trailing "\`\`\`json" and "\`\`\`":
 
```json
{
	"key1_to_extract": type of key1_to_extract  // key1 extraction instruction.
	"key2_to_extract": type of key2_to_extract  // key2 extraction instruction.
}
```\#add by me ignore the '\'

את זה נשלח כמשתנה לתבנית הפרומפט.

פרמוט הפלט

כשקיבלנו פלט מהמודל, נשתמש במפרסר שהשתמשנו בו קודם להעביר את הפלט ממחרוזת למבנה שרצינו.

response = chat(messages)
output_dict = output_parser.parse(response.content)

זיכרון (Memory)

רכיב מרכזי בLangChain הוא הזכרון (Memory). כשאנחנו מדברים על הזכרון אנחנו כמובן מדברים על הזכרון של הצ’אט, שנאמר בפשטות שהוא היסטוריית הצאט אבל לא בדיוק (כי לעיתים נרצה לשמור חלקים). יש מוטיבציות שונות לנהל את הזכרון, בעיקר בגלל מגבלת ההקשר (context) שנתן להכניס למודלים, אך ישנן סיבות נוספות כמו חסכון בעלויות. נשים לב שניתן להשתמש בכמה סוגי זכרונות בו זמנית.

שיחה כשרשרת

למרות שאת מושג השרשרת (Chain) נלמד בהרחבה רק בפרק הבא, נשתמש בו כבר כאן כי ניתן לנהל איתו שיחה (Conversation) ביתר נוחות. נאתחל שרשרת שיחה עם זיכרון (כאשר llm_model משתנה שמכיל את שם המודל):

from langchain.chat_models import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
 
llm = ChatOpenAI(temperature=0.0, model=llm_model)
memory = ConversationBufferMemory()
conversation = ConversationChain(
    llm=llm, 
    memory = memory,
    verbose=True
)

נשים לב שמשתנה התמלול (verbose) מאותחל בברירת מחדל להיות כבוי. אפשר להדליק אותו כדי לראות את מהלך השיחה. נתחיל שיחה:

conversation.predict(input="Hi")

מבנה הזכרון

נשים לב שהתמשנו בזכרון מסוג זיכרון באפר שיחה (ConversationBufferMemory) ולכן כל הזכרון ישמר לנו בגבולות ההקשר של המודל. נוכל לראות את הבאפר באמצעות:

print(memory.buffer)

ונוכל לראות את משתני הזיכרון באמצעות:

memory.load_memory_variables({})

נוכל לאפס ולכתוב ישירות את הזיכרון:

memory = ConversationBufferMemory()
memory.save_context({"input": "Hi"}, 
                    {"output": "What's up"})

סוגי זיכרון

סיכום סוגי זכרון

  • זיכרון באפר שיחה (ConversationBufferMemory)
    • זיכרון זה מאפשר אחסון של הודעות ולאחר מכן מחלץ את ההודעות לתוך משתנה.
  • זיכרון חלון באפר שיחה (ConversationBufferWindowMemory)
    • זיכרון זה שומר רשימה של האינטרקציות במהלך השיחה לאורך זמן. הוא משתמש רק ב-K האינטרקציות האחרונות.
  • זיכרון באפר טוקנים שיחה (ConversationTokenBufferMemory)
    • זיכרון זה שומר באפר של אינטרקציות אחרונות בזיכרון, ומשתמש באורך הטוקנים ולא במספר האינטרקציות כדי לקבוע מתי לרוקן את האינטרקציות.
  • זיכרון סיכום שיחה (ConversationSummaryMemory)
    • זיכרון זה יוצר סיכום של השיחה לאורך זמן.

זיכרון חלון באפר שיחה (ConversationBufferWindowMemory)

זיכרון זה שומר את מספר האינטרקציות בשיחה, כאשר אינטרקציה היא חלון הודעות של המשתתפים כאשר כל משתתף שולח לכל היותר הודעה. הפרמטר הרלוונטי בזכרון זה הוא כמובן k שהוא מספר האינטרקציות מסוף השיחה.

from langchain.memory import ConversationBufferWindowMemory
 
memory = ConversationBufferWindowMemory(k=1)

זיכרון באפר טוקנים שיחה (ConversationTokenBufferMemory)

הפרמטר הרלוונטי בזכרון זה הוא כמובן max_token_limit, אך פרמטר נוסף נדרש הוא llm מפני שמודלים שונים משתמשים בTokenizer שונה.

from langchain.memory import ConversationTokenBufferMemory
 
memory = ConversationTokenBufferMemory(llm=llm, max_token_limit=50)

זיכרון סיכום שיחה (ConversationSummaryMemory)

זכרון מסכם את באפר הזכרון כאשר הוא עולה על max_token_limit ושומר אותו כהודעת מערכת. כמו בזכרון הקודם גם כאן הפרמטר הרלוונטי הוא max_token_limit, אך פרמטר נוסף נדרש הוא llm.

from langchain.memory import ConversationSummaryBufferMemory
 
memory = ConversationSummaryBufferMemory(llm=llm, max_token_limit=100)

סוגי זיכרון נוספים

מפני שהקורס הוא בסיסי למתחילים ישנם זיכרונות חשובים נוספים שלא הורחב עליהם אך נציין אותם.

זיכרון אחזור מחסן וקטורים (VectorStoreRetrieverMemory)

מאחסן טקסט (מהשיחה או ממקומות נוספים) בVectorDB ומאחזר את קטעי הטקסט הרלוונטיים ביותר.

זכרון ישויות (ConversationEntityMemory)

משתמש בLLM לחלץ עובדות על ישויות במהלך השיחה ובונה את הידע שלו לגביהם.

שרשראות (chains)

שרשראות הן אבן הבניין המרכזית ובחשובה ביותר בLangChain. שרשרת בד”כ מחברת בין מודל LLM לבין פרפומפט כך שניתן להתייחס אליה כאובייקט אחד כאבן בניין ולחבר אותה לאבני בניין אחרים.

שרשרת LLM (LLMChain)

שרשרת פשוטה ביותר אבל חזקה ושימושית מאוד המחברת בין LLM לפרומפט. לדוגמא:

from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.chains import LLMChain
 
llm = ChatOpenAI(temperature=0.9, model="gpt-3.5-turbo")
prompt = ChatPromptTemplate.from_template(
    "... {var}?"
)
chain = LLMChain(llm=llm, prompt=prompt)
 
chain.run("...")

שרשרת סדרתית (Sequential Chain)

שרשרת סדרתית היא שרשת המאפשרת לשלב מספר שרשראות שבהן הפלט של שרשרת אחת הוא הקלט לשרשרת הבאה. ישנם שני סוגים של שרשראות עוקבות:

  1. שרשרת פשוטה סדרתית (SimpleSequentialChain): קלט/פלט יחיד.
  2. שרשרת סדרתית (SequentialChain): קלטים/פלטים מרובים.

דוגמאות קוד

שרשרת פשוטה סדרתית

from langchain.chains import SimpleSequentialChain
 
llm = ChatOpenAI(temperature=0.9, model="gpt-3.5-turbo")
 
# prompt template 1
first_prompt = ChatPromptTemplate.from_template(
    "What is the best name to describe \
    a company that makes {product}?"
)
 
# Chain 1
chain_one = LLMChain(llm=llm, prompt=first_prompt)
 
# prompt template 2
second_prompt = ChatPromptTemplate.from_template(
    "Write a 20 words description for the following \
    company:{company_name}"
)
 
# chain 2
chain_two = LLMChain(llm=llm, prompt=second_prompt)
 
overall_simple_chain = SimpleSequentialChain(chains=[chain_one, chain_two], verbose=True )
 
overall_simple_chain.run(product) # return output only

שרשרת סדרתית

from langchain.chains import SequentialChain
 
llm = ChatOpenAI(temperature=0.9, model=llm_model)
 
# prompt template 1: translate to english
first_prompt = ChatPromptTemplate.from_template(
    "Translate the following review to english:"
    "\n\n{Review}"
)
# chain 1: input= Review and output= English_Review
chain_one = LLMChain(llm=llm, prompt=first_prompt, output_key="English_Review" )
 
second_prompt = ChatPromptTemplate.from_template(
    "Can you summarize the following review in 1 sentence:"
    "\n\n{English_Review}"
)
# chain 2: input= English_Review and output= summary
chain_two = LLMChain(llm=llm, prompt=second_prompt, output_key="summary" )
 
# prompt template 3: translate to english
third_prompt = ChatPromptTemplate.from_template(
    "What language is the following review:\n\n{Review}"
)
# chain 3: input= Review and output= language
chain_three = LLMChain(llm=llm, prompt=third_prompt, output_key="language" )
 
# prompt template 4: follow up message
fourth_prompt = ChatPromptTemplate.from_template(
    "Write a follow up response to the following "
    "summary in the specified language:"
    "\n\nSummary: {summary}\n\nLanguage: {language}"
)
# chain 4: input= summary, language and output= followup_message
chain_four = LLMChain(llm=llm, prompt=fourth_prompt, output_key="followup_message" )
 
# overall_chain: input= Review 
# and output= English_Review,summary, followup_message
overall_chain = SequentialChain(
    chains=[chain_one, chain_two, chain_three, chain_four],
    input_variables=["Review"],
    output_variables=["English_Review", "summary","followup_message"],
    verbose=True
)
 
review = df.Review[5]
overall_chain(review) # return input and output as json

שרשרת נתב (Router Chain)

פעולה נפוצה בסיסית היא לנתב קלט של שרשרת באופן שתלוי במה בקלט. דרך נוחה להמחיש את זה היא שיש כמות גדולה של תתי שרשראות שכל אחד מתמחה בתחום ספציפי ויש שרשרת שמנתבת באופן מקדים לאיזה תת שרשרת להעביר ומעבירה.

דוגמאת קוד

נבנה את תתי השרשראות שלנו:

physics_template = """You are a very smart physics professor. \
You are great at answering questions about physics ...
 
Here is a question:
{input}"""
 
math_template = """You are a very good mathematician. \
You are great at answering math questions...
 
Here is a question:
{input}"""
 
history_template = """You are a very good historian. \
You have an excellent knowledge of and understanding of people,\
events and contexts from a range of historical periods. ...
 
Here is a question:
{input}"""
 
computerscience_template = """ You are a successful computer scientist.\ ...
You are great at answering coding questions. \
 
Here is a question:
{input}"""
 
prompt_infos = [
    {
        "name": "physics", 
        "description": "Good for answering questions about physics", 
        "prompt_template": physics_template
    },
    {
        "name": "math", 
        "description": "Good for answering math questions", 
        "prompt_template": math_template
    },
    {
        "name": "History", 
        "description": "Good for answering history questions", 
        "prompt_template": history_template
    },
    {
        "name": "computer science", 
        "description": "Good for answering computer science questions", 
        "prompt_template": computerscience_template
    }
]

נשים לב שיש לנו שם לתת השרשרת name, תאור description שבעזרתו שרשרת הנתב מקבלת את ההחלטה מיהו מבין תתי השרשראות הוא המתאים ביותר, ופרומפט שישמש את תת השרשרת prompt_template כמובן.

כעת נייצר מילון של תתי השרשראות שלנו:

 
from langchain.chains.router import MultiPromptChain
from langchain.chains.router.llm_router import LLMRouterChain,RouterOutputParser
from langchain.prompts import PromptTemplate
 
llm = ChatOpenAI(temperature=0, model=llm_model)
 
destination_chains = {}
for p_info in prompt_infos:
    name = p_info["name"]
    prompt_template = p_info["prompt_template"]
    prompt = ChatPromptTemplate.from_template(template=prompt_template)
    chain = LLMChain(llm=llm, prompt=prompt)
    destination_chains[name] = chain
    
destinations = [f"{p['name']}: {p['description']}" for p in prompt_infos]
destinations_str = "\n".join(destinations)
 
default_prompt = ChatPromptTemplate.from_template("{input}")
default_chain = LLMChain(llm=llm, prompt=default_prompt)
 
MULTI_PROMPT_ROUTER_TEMPLATE = """Given a raw text input to a \
language model select the model prompt best suited for the input. \
You will be given the names of the available prompts and a \
description of what the prompt is best suited for. \
You may also revise the original input if you think that revising\
it will ultimately lead to a better response from the language model.
 
<< FORMATTING >>
Return a markdown code snippet with a JSON object formatted to look like:
```json
{{{{
    "destination": string \ name of the prompt to use or "DEFAULT"
    "next_inputs": string \ a potentially modified version of the original input
}}}}
```\
 
REMEMBER: "destination" MUST be one of the candidate prompt \
names specified below OR it can be "DEFAULT" if the input is not\
well suited for any of the candidate prompts.
REMEMBER: "next_inputs" can just be the original input \
if you don't think any modifications are needed.
 
<< CANDIDATE PROMPTS >>
{destinations}
 
<< INPUT >>
{{input}}
 
<< OUTPUT (remember to include the ```json)>>"""
router_template = MULTI_PROMPT_ROUTER_TEMPLATE.format(
    destinations=destinations_str
)
router_prompt = PromptTemplate(
    template=router_template,
    input_variables=["input"],
    output_parser=RouterOutputParser(),
)
 
router_chain = LLMRouterChain.from_llm(llm, router_prompt)
 
chain = MultiPromptChain(router_chain=router_chain, 
                         destination_chains=destination_chains, 
                         default_chain=default_chain, verbose=True
                        )
 
chain.run("What is black body radiation?")
chain.run("what is 2 + 2")
chain.run("Why does every cell in our body contain DNA?")

מענה לשאלות (Question And Answer)

אחד השימושים המורכבים הנפוצים ביותר לLLM הוא בניית מערכת של שאילת שאלות על מסמכים של קטעי טקסט כגון שחולץ מPDF או מעמודי אינטרנט או ממארגי מידע פנימיים, כך שבאמצעות שימוש בLLM ניתן להנגיש למשתמש הבנה עמוקב של המידע הרלוונטי ונגישות ישירה לצורך שלו. זה כח משמעותי כי זה מחבר LLM עם מידע שהוא לא אומן עליו, זה הופך אותו לגמיש יותר. חלק זה מכיל רכיבים מעבר לרכיבי הבסיס של מודלי שפה שהם מרכזיים בLangchain כמו מודלי קידוד (Embedding) ומחסני וקטורים (VectoreStores) .

דוגמת קוד מהירה

כאן אנחנו מייבאים את כלל הרכיבים הרלוונטיים, עליהם נסביר בהמשך:

from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
from langchain.document_loaders import CSVLoader
from langchain.vectorstores import DocArrayInMemorySearch
from IPython.display import display, Markdown
from langchain.llms import OpenAI

באמצעות VectorstoreIndexCreator נאתחל Vectorstore באופן מיידי שישמר בזכרון, ונטען את הקובץ באופן ישיר עם CSVLoader המאותחל כך:

from langchain.indexes import VectorstoreIndexCreator
 
file = '... .csv'
loader = CSVLoader(file_path=file)
 
index = VectorstoreIndexCreator(
    vectorstore_cls=DocArrayInMemorySearch
).from_loaders([loader])

ואז כדי לקבל תשובה נדרש לבצע רק:

response = index.query(query)
display(Markdown(response))

הסבר כללי

אחת המגבלות המרכזיות של LLM היא שהם יכולים לקבל אלפי טוקנים בודדים (כיום עשרות אלפי), כשיש לנו קבצים עצומים, איך ניתן להשתמש בLLM כדי לענות על שאלות עליהם? לשם כך יש צורך בקידוד ומסני וקטורים.

מודלי קידוד (Embedding)

קידוד הוא יצור נומרי של טקסט, כך שהוא מייצג את המשמעות של תוכן הטקסט, ווקטורים בעלי משמעות דומה יהיו קרובים זה לזה. זה משמעותי לבחירת הוקטורים אותם נרצה להעביר לLLM.

מחסני וקטורים (Vector Database)

מחסני נתונים וקטוריים הם הדרך לאחסן את הקידודים. הדרך שבו ניצור אותו זה חלוקת מסמך לחלקים Chunks כאשר מקבלים מסמך גדול.

התהליך

כאשר נקבל מסמך גדול נרצה לחלק אותו לחלקים קטנים ואותם לאחסן במחסן נתונים, כאשר לכל חלק נייצר קידוד שהוא יהיה האינדקס. כאשר נקבל שאילת נחשב את הקידוד שלה ונחזיר את הקרובים ביותר שהם הרלוונטים ביותר שאותם נעביר למודל כדי לקבל תשובה סופית.

דוגמת קוד מפורטת

טעינת מסמך

תחילה נאתחל טוען מסמכים, ודוגמא לשימוש:

from langchain.document_loaders import CSVLoader
loader = CSVLoader(file_path=file)
 
docs = loader.load()
docs[0]

קידוד

לאחר מכן נאתחק את המקודד שלנו, ודוגמא לשימוש:

from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
 
embed = embeddings.embed_query("Hi my name is Harrison")

מחסן וקטורים

כעת ניצור מחסן נתונים וקטורי:

db = DocArrayInMemorySearch.from_documents(
    docs, 
    embeddings
)

כעת נוכל לבצע חיפוש סמנטי:

docs = db.similarity_search(query)

מאחזר (retriver)

כדי להשתמש במחסן הוקטורים לשאילת שאלות, נצטרך מאחזר, ניצור:

retriever = db.as_retriever()

מענה לשאלה

כעת כשיש לנו את כל היסודות , ניתן לחבר הכל:

llm = ChatOpenAI(temperature = 0.0)
 
qdocs = "".join([docs[i].page_content for i in range(len(docs))])
 
response = llm.call_as_llm(f"{qdocs} Question: ...") 
 
display(Markdown(response))

מאחזר שאלה תשובה (RetrievalQA)

במקום לעשות את כל השלבים האלו, ניתן להשתמש בשרשרת יעודית של Langchain.

qa_stuff = RetrievalQA.from_chain_type(
    llm=llm, 
    chain_type="stuff",
    retriever=retriever, 
    verbose=True
)

נשים לב שבסוג השרשרת בחרנו ב stuff שזו השיטה הפשוטה ביותר שפשוט משרשרת את תוצאות שאוחזרו ומעבירה לLLM, ישנם נוספים שנדבר עליהם בהמשך. נשים לב שבדוגמת קוד מהירה לעיל, גם עשינו הרבה שלבים שכתבנו כאן באריכות, בקצרה ובפשטות.

שרשראות אגריגציה לחלקים

במקרה בו אחזרנו מספר רב של חלקים אותם אנחנו רוצים להעביר לLLM כדי לקבל תשובה סופית, ישנם כמה שיטות כיצד לבצע אגריגציה למספר רב זה של חלקים. שיטת Stuff למקרה שהמסמכים המאוחזרים במקבלת ההקשר, ו .. במקרים אחרים.

שיטת Stuff

השיטה הפשוטה היותר היא במקרה בו הכמות נכנסת להקשר, במקרה זה פשוט שנרשר אותם בפרומפט אחד ונעביר לLLM.

יתרונות

מבצע קריאה יחידה לLLM, לLLM יש גישה לכל הנתונים בפעם אחת.

חסרונות

לLLM יש מגבלת הקשר, וזה לא יעבוד טוב במקרה של מסמכים גדולים או הרבה מסמכים.

שיטת Map-Reduce

בשיטת Map-Reduce אנחנו מבצעים קריאה לLLM על כל אוסף של חלקים במקביל, ומבקשים ממנו תוצאה לכל קבוצה, ואת כלל התוצאות אנחנו מכניסים לLLM ומבקשים ממנו תוצאה על התוצאות.

שיטת Refine

בשיטת Refine אנחנו מבצעים קריאה לLLM על אוסף של חלקים, ואז באופן איטרטיבי לוקחים את הפלט של התוצאה של הLLM יחד עם אוסף חדש, עד שכלל האוספים נגמרים.

שיטת Map-Rerank

שיטת Map-Rerank דומה לשיטת Map-Reduce רק שבנוסף לבקשה לתוצאה אנחנו מבקשים מהמודל גם ציון לאיכות התוצאה, ואז לוקחים את התוצאות הגבוהות ביותר לחישוב הפלט הסופי.

הערכה (Evaluation)

אחד המרכיבים החשובים ב… הוא הערכת ביצועי המודל. ניתן לעשות זאת ידנית בעין, אך לעיתים כאשר המשימה מתאימה לכך, ניתן לתת למודלים להעריך את ביצועי המודל הראשון. נמחיש את התהליך בפרק זה באמצעות דוגמא להערכת ביצועי RAG.

דוגמא

נתחיל עם הקוד מהפרק הקודם:

from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
from langchain.document_loaders import CSVLoader
from langchain.vectorstores import DocArrayInMemorySearch
from IPython.display import display, Markdown
from langchain.llms import OpenAI
from langchain.indexes import VectorstoreIndexCreator
 
file = '... .csv'
loader = CSVLoader(file_path=file)
 
index = VectorstoreIndexCreator(
    vectorstore_cls=DocArrayInMemorySearch
).from_loaders([loader])
 
llm = ChatOpenAI(temperature = 0.0, model=llm_model)
qa = RetrievalQA.from_chain_type(
    llm=llm, 
    chain_type="stuff", 
    retriever=index.vectorstore.as_retriever(), 
    verbose=True,
    chain_type_kwargs = {
        "document_separator": "<<<<>>>>>"
    }
)

הדרך הנאיבית היא לייצר דוגמאות באופן ידני, עבור דוגמאות שנסתכל בעין בצורה הבאה:

examples = [
    {
        "query": "...",
        "answer": "..."
    },
    {
        "query": "...",
        "answer": "..."
    }
]

אך קשה לייצר מזה סדרי גודל, והיינו רוצים להפוך את התהליך לאוטומטי. אז ניתן לעשות את זה אוטומטי באמצעות מודלים, ואפילו יש chain יעודי לזה:

from langchain.evaluation.qa import QAGenerateChain
example_gen_chain = QAGenerateChain.from_llm(ChatOpenAI(model=llm))
 
new_examples = example_gen_chain.apply_and_parse(
    [{"doc": t} for t in data[:5]]
)
 
examples += new_examples

וכך ניצור 5 דוגמאות שאיתם נוכל להעריך בהמשך ונוסיף לדוגמאות הישנות שלנו. נמשיך את התהליך. ננסה להעריך באופן ידני את תוצאת התהליך:

qa.run(examples[0]["query"])

במצב זה אנחנו מקבלים ישירות את התשובה הסופית, וכדי לראות את התהליך נדליק את משתנה הdebug ונריץ מחדש.

import langchain
langchain.debug = True
 
qa.run(examples[0]["query"])

כעת נוכל לראות את תהליך הRAG ואת שלבי ההגעה לתשובה בצורה טכנית מפורטת. חשוב לזכור לכבות בסוף התהליך את מצב הdebug .

כעת ניתן למודל אחר להעריך את האחזור של המודל הראשון. נחשב את התוצאות:

predictions = qa.apply(examples)

כעת נייצר הערכה של התוצאות אל מול הדוגמאות האמיתיות:

from langchain.evaluation.qa import QAEvalChain
 
llm = ChatOpenAI(temperature=0, model=llm_model)
eval_chain = QAEvalChain.from_llm(llm)
 
graded_outputs = eval_chain.evaluate(examples, predictions)

נדפיס את טיב ההערכה שלנו:

for i, eg in enumerate(examples):
    print(f"Example {i}:")
    print("Question: " + predictions[i]['query'])
    print("Real Answer: " + predictions[i]['answer'])
    print("Predicted Answer: " + predictions[i]['result'])
    print("Predicted Grade: " + graded_outputs[i]['text'])
    print()

נשים לב שאנחנו נדרשים למודל בהערכה כאן, מפני שנקבל כאן 2 תשובות שבשיטה שאינה הבנת השפה לא היינו יכולים להבין שהן תשובות זהות, וזו חשיבות המודל בתהליך.

בסוף השיעור הוא מציג את מה שהיום הוא LangSmith

סוכנים (Agents)

לעתים אנשים חושבים על LLMs כמאגרי מידע גדולים עם יכולת מענה. דרך יותר שימושית להסתכל עליהם, זה כעל מנוע בעל יכולת הבנה (Reasoning) שלדוגמא ניתן לתת לו קטעים ממקורות מידע חיצוניים והוא יוכל להשתמש במידע שיש לו ואולי במידע החיצוני החדש הזה יחד כדי לתת מענה (לענות לשאלה, לגבש הבנה כלפי תוכן או אפילו להחליט מה לעשות בצעד הבא). וזה הבסיס מאחורי הסוכנים (Agents) של Langchain. הסוכנים הם החלק המעניין יותר בLangchain אך יחד עם זאת החדש ביותר והצומח ביותר. ולכן יהיו אוסף של נושאים ויכולות שהם חדשים לכולם בענף.

נבין את הנושא באמצעות דוגמה.

דוגמה להמחשה

כשמאתחילם סוכן, מפני מה שחשוב לנו בו זו ההבנה שלו ונצטרך שיהיה מדויק ביותר, נקבע את הטמפרטורה להיות 0.

from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(temperature=0)

אתחול הסוכן

כעת נאתחל כלים (tools) כדי שהסוכן יוכל להשתמש בהם. ניתן לסוכן כלי לבצע פעולות מתמטיות באמצעות llm וגישה לוויקיפדיה. הכלי הראשון הוא chain בעצמו, והשני הוא אפשרות לבצע קריאות API. בנוסף נאתחל סוכן, כאשר נביא לו את הכלים שאתחלנו, ונבחר בסוג שלו CHAT_ZERO_SHOT_REACT_DESCRIPTION - כלומר סוכן שעובד טוב עם מודלים מסוג צ’אט, שפועל ללא דוגמאות המבוסס על שיטת ReAct .

from langchain.agents import load_tools, initialize_agent
from langchain.agents import AgentType
 
tools = load_tools(["llm-math","wikipedia"], llm=llm)
 
agent= initialize_agent(
   tools, 
   llm, 
   agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION,
   handle_parsing_errors=True,
   verbose = True)

נבחר גם ב handle_parsing_errors=True, שמבצע תיקון במידה והפלט של המודל יצא בפורמט לא נכון ומבקש לתקן אותו, ובverbose = True שיציג לנו את התהליך של פעולות הסוכן במחברת.

הרצה

דוגמה: פעולת חשבון

נריץ דוגמת חישוב:

agent("What is the 25% of 300?")

נראה את תהליך הפעולה של הסוכן:

> Entering new AgentExecutor chain...
Thought: To find 25% of 300, we can use a calculator to perform the calculation.
 
Action:
``` \
{
  "action": "Calculator",
  "action_input": "25% * 300"
}
``` \
 
Observation: Answer: 75.0
Thought:I now know the final answer
 
Final Answer: 75.0
 
> Finished chain.
 
{'input': 'What is the 25% of 300?', 'output': '75.0'}

הסוכן מבצע מחשבה (Thought) מה הצעד הבא שהוא צריך לעשות. הוא בוחר בפעולה (Action) של מחשבון, ומייצר אבחנה (Observation) שהיא התוצאה של הכלי, וככה ממשיך בלולאה. נשים לב שהסוכן בחר לעשות פעולה, והחזיר JSON של שם הפעולה, והקלט שנדרש להכניס לכלי. כאשר הוא מסיים הוא רושם את הקלט, במקרה זה השאלה שהוא קיבל, ואת הפלט, התשובה שהוא הסיק. נציין שכך פועל סוכן מסוג ReAct באופן ספציפי.

דוגמה: חיפוש בוויקיפדיה

נשאל את הסוכן שאלה

question = "Tom M. Mitchell is an American computer scientist \
and the Founders University Professor at Carnegie Mellon University (CMU)\
what book did he write?"
result = agent(question) 

נראה את פעולת הסוכן:

> Entering new AgentExecutor chain...
Thought: I will use Wikipedia to find out which book Tom M. Mitchell wrote.
Action:
``` \
{ 
  "action": "Wikipedia",
  "action_input": "Tom M. Mitchell"
}
``` \
Observation: Page: Tom M. Mitchell
Summary: Tom Michael Mitchell (born August 9, 1951) is an American computer scientist and the Founders University Professor at Carnegie Mellon University (CMU). (...)
 
 
Page: Ensemble learning
Summary: In statistics and machine learning, ensemble methods use multiple learning algorithms to obtain better predictive performance than could be obtained from any of the constituent learning algorithms alone. (...)
 
Thought:I have found that Tom M. Mitchell wrote the textbook "Machine Learning."
Final Answer: Machine Learning
 
> Finished chain.

הסוכן מבצע אבחנה שהוא צריך לחפש בוויקיפדיה, בוחר בכלי ומכין את שאילתת החיפוש. מחפש ומקבל 2 דפים שאחד מהם רלוונטי, ומבצע מחשבה שהוא מצא את מה שהוא היה צריך, ומחזיר תשובה סופית. הערה: בסרטון הוא הגיע לתשובה הסופית בשלב הראשון ולמרות זאת חיפש את הספר עצמו, וזו הייתה דוגמה טובה לכך שמודלי השפה לא מושלמים עדיין.

דוגמה: הרצת קוד

אחד הדברים שמאוד שימושי לעשות, זה לתת לLLM לכתוב קוד ולתת לו את הכלי להריץ אותו, הנה דוגמה לאיך עושים את זה עם לתת לו להריץ פייתון:

from langchain.agents.agent_toolkits import create_python_agent
from langchain.tools.python.tool import PythonREPLTool
 
agent = create_python_agent(
    llm,
    tool=PythonREPLTool(),
    verbose=True
)
 
customer_list = [["Harrison", "Chase"], 
                 ["Lang", "Chain"],
                 ["Dolly", "Too"],
                 ["Elle", "Elem"], 
                 ["Geoff","Fusion"], 
                 ["Trance","Former"],
                 ["Jen","Ayai"]
                ]
                
agent.run(f"""Sort these customers by \
last name and then first name \
and print the output: {customer_list}""") 

בדוגמה הזו אנחנו מבקשים מהסוכן למיין רשימת לקוחות לפי סדר אלפביתי, וזה מה שהסוכן בוחר לעשות:

> Entering new AgentExecutor chain...
We can use the sorted() function in Python to sort the list of customers based on last name first and then first name.
Action: Python REPL
Action Input: customers = [['Harrison', 'Chase'], ['Lang', 'Chain'], ['Dolly', 'Too'], ['Elle', 'Elem'], ['Geoff', 'Fusion'], ['Trance', 'Former'], ['Jen', 'Ayai']]
sorted_customers = sorted(customers, key=lambda x: (x[1], x[0]))
print(sorted_customers)
Observation: [['Jen', 'Ayai'], ['Lang', 'Chain'], ['Harrison', 'Chase'], ['Elle', 'Elem'], ['Trance', 'Former'], ['Geoff', 'Fusion'], ['Dolly', 'Too']]
Thought:The customers have been sorted by last name and then first name.
Final Answer: [['Jen', 'Ayai'], ['Lang', 'Chain'], ['Harrison', 'Chase'], ['Elle', 'Elem'], ['Trance', 'Former'], ['Geoff', 'Fusion'], ['Dolly', 'Too']]
 
> Finished chain.
 
"[['Jen', 'Ayai'], ['Lang', 'Chain'], ['Harrison', 'Chase'], ['Elle', 'Elem'], ['Trance', 'Former'], ['Geoff', 'Fusion'], ['Dolly', 'Too']]"

הסוכן ביצע אבחנה שהוא יכול להשתמש בקוד פייתון בכדי למיין את המחרוזת וזה מה שהוא בחר לעשות.

דיבוג סוכן

לעיתים, נרצה לדעת מה בדיוק קורה בצורה מפורטת ביותר בפעולות של הסוכן. לצורך זה נדליק את משתנה הdebug ונכבה מיד בסיום, לדוגמא:

import langchain
langchain.debug=True
agent.run(f"""Sort these customers by \
last name and then first name \
and print the output: {customer_list}""") 
langchain.debug=False

הרצה של דוגמא זו תראה בפרוט על כל chain שמתבצע את הפרטים המדויקים שלו בפרוט. ניתן לראות כך כיצד ממומשים הכלים שמשתמשים בהם ואת הפרומפטים של הchain-ים.

בניית כלי custom לסוכן

במקרים רבים נרצה לבנות כלי לסוכן מותאם למקרה שלנו, נראה דוגמה כיצד לעשות זאת, נייצר כלי שמחזיר את התאריך של היום:

from langchain.agents import tool
from datetime import date
 
@tool
def time(text: str) -> str:
    """Returns todays date, use this for any \
    questions related to knowing todays date. \
    The input should always be an empty string, \
    and this function will always return todays \
    date - any date mathmatics should occur \
    outside this function."""
    return str(date.today())

נשים לב שתיאור הפונקצייה המוגדרת ככלי תשתמש את הסוכן להבין מה הכלי עושה. נוסיף אותו לכלים שלנו לסוכן, ונריץ:

agent= initialize_agent(
    tools + [time], 
    llm, 
    agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION,
    handle_parsing_errors=True,
    verbose = True)

ואז נקבל:

> Entering new AgentExecutor chain...
Thought: I can use the `time` tool to find out today's date.
Action:
``` \
{
  "action": "time",
  "action_input": ""
}
``` \
Observation: 2024-07-03
Thought:Final Answer: 2024-07-03
 
> Finished chain.