一篇文章搞懂 LangChain

在日常生活中,我們通常致力於構建端到端的應用程序。有許多自動機器學習平臺和持續集成 / 持續交付(CI/CD)流水線可用於自動化我們的機器學習流程。我們還有像 Roboflow 和 Andrew N.G. 的 Landing AI 這樣的工具,可以自動化或創建端到端的計算機視覺應用程序。

如果我們想要藉助 OpenAI 或 Hugging Face 創建基於大語言模型的應用程序,以前我們可能需要手動完成。現在,爲了實現相同的目標,我們有兩個最著名的庫,即 Haystack 和 LangChain,它們可以幫助我們創建基於大語言模型的端到端應用程序或流程。

下面讓我們深入地瞭解一下 LangChain。

01 什麼是 LangChain?

LangChain 是一種創新框架,正在徹底改變我們開發由語言模型驅動的應用程序的方式。 通過引入先進的原理,LangChain 正在重新定義傳統 API 所能實現的限制。此外,LangChain 應用程序具有智能代理的特性,使語言模型能夠與環境進行互動和自適應。

LangChain 由多個模塊組成。正如其名稱所示,LangChain 的主要目的是將這些模塊進行鏈式連接。這意味着我們可以將每個模塊都串聯在一起,並使用這個鏈式結構一次性調用所有模塊。

這些模塊由以下部分組成:

Model

正如介紹中所討論的那樣,模型主要涵蓋大語言模型(LLM)。大語言模型是指具有大量參數並在大規模無標籤文本上進行訓練的神經網絡模型。科技巨頭們推出了各種各樣的大型語言模型,比如:

藉助 LangChain,與大語言模型的交互變得更加便捷。 LangChain 提供的接口和功能有助於將 LLM 的強大能力輕鬆集成到你的工作應用程序中。LangChain 利用 asyncio 庫爲 LLM 提供異步支持。

對於需要同時併發調用多個 LLM 的網絡綁定場景,LangChain 還提供了異步支持。通過釋放處理請求的線程,服務器可以將其分配給其他任務,直到響應準備就緒,從而最大限度地提高資源利用率。

目前,LangChain 支持 OpenAI、PromptLayerOpenAI、ChatOpenAI 和 Anthropic 等模型的異步支持,但在未來的計劃中將擴展對其他 LLM 的異步支持。你可以使用 agenerate 方法來異步調用 OpenAI LLM。此外,你還可以編寫自定義的 LLM 包裝器,而不僅限於 LangChain 所支持的模型。

我在我的應用程序中使用了 OpenAI,並主要使用了 Davinci、Babbage、Curie 和 Ada 模型來解決我的問題。每個模型都有其自身的優點、令牌使用量和使用案例。

更多關於這些模型的信息請閱讀:

https://subscription.packtpub.com/book/data/9781800563193/2/ch02lvl1sec07/introducing-davinci-babbage-curie-and-ada

案例 1:

# Importing modules 
from langchain.llms import OpenAI

#Here we are using text-ada-001 but you can change it 
llm = OpenAI(model_, n=2, best_of=2)

#Ask anything
llm("Tell me a joke")

輸出 1:

'\n\nWhy did the chicken cross the road?\n\nTo get to the other side.'

案例 2:

llm_result = llm.generate(["Tell me a poem"]*15)

輸出 2:

[Generation(text="\n\nWhat if love neverspeech\n\nWhat if love never ended\n\nWhat if love was only a feeling\n\nI'll never know this love\n\nIt's not a feeling\n\nBut it's what we have for each other\n\nWe just know that love is something strong\n\nAnd we can't help but be happy\n\nWe just feel what love is for us\n\nAnd we love each other with all our heart\n\nWe just don't know how\n\nHow it will go\n\nBut we know that love is something strong\n\nAnd we'll always have each other\n\nIn our lives."),  
 Generation(text='\n\nOnce upon a time\n\nThere was a love so pure and true\n\nIt lasted for centuries\n\nAnd never became stale or dry\n\nIt was moving and alive\n\nAnd the heart of the love-ick\n\nIs still beating strong and true.')]

Prompt

衆所周知,提示(prompt)是我們向系統提供的輸入,以便根據我們的使用案例對答案進行精確或特定的調整。許多時候,我們希望得到的不僅僅是文本,還需要更結構化的信息。基於對比預訓練和零樣本學習的許多新的目標檢測和分類算法都將提示作爲有效的輸入來進行結果預測。舉例來說,OpenAI 的 CLIP 和 META 的 Grounding DINO 都使用提示作爲預測的輸入。

在 LangChain 中,我們可以根據需要設置提示模板,並將其與主鏈相連接以進行輸出預測。 此外,LangChain 還提供了輸出解析器的功能,用於進一步精煉結果。輸出解析器的作用是(1)指導模型輸出的格式化方式,和(2)將輸出解析爲所需的格式(包括必要時的重試)。

在 LangChain 中,我們可以提供提示模板作爲輸入。 模板指的是我們希望獲得答案的具體格式或藍圖。LangChain 提供了預先設計好的提示模板,可以用於生成不同類型任務的提示。然而,在某些情況下,預設的模板可能無法滿足你的需求。在這種情況下,我們可以使用自定義的提示模板。

案例:

from langchain import PromptTemplate
# This template will act as a blue print for prompt

template = """
I want you to act as a naming consultant for new companies.
What is a good name for a company that makes {product}?
"""

prompt = PromptTemplate(
    input_variables=["product"],
    template=template,
)
prompt.format(product="colorful socks")
# -> I want you to act as a naming consultant for new companies.
# -> What is a good name for a company that makes colorful socks?

Memory

在 LangChain 中,鏈式和代理默認以無狀態模式運行,即它們獨立處理每個傳入的查詢。然而,在某些應用程序(如聊天機器人)中,保留先前的交互記錄對於短期和長期都非常重要。這時就需要引入 “內存” 的概念。

LangChain 提供兩種形式的內存組件。首先,LangChain 提供了輔助工具,用於管理和操作先前的聊天消息,這些工具設計成模塊化的,無論用例如何,都能很好地使用。其次,LangChain 提供了一種簡單的方法將這些工具集成到鏈式結構中,使其非常靈活和適應各種情況。

案例:

from langchain.memory import  ChatMessageHistory  
  
history  = ChatMessageHistory()  
history.add_user_message("hi!")  
  
history.add_ai_message("whats up?")  
history.messages

輸出:

[HumanMessage(content='hi!'additional_kwargs={}),  
 AIMessage(content='whats up?'additional_kwargs={})]

Chain

鏈式(Chain)提供了將各種組件合併成一個統一應用程序的方式。例如,可以創建一個鏈式,它接收用戶的輸入,並使用 PromptTemplate 對其進行格式化,然後將格式化後的回覆傳遞給 LLM(大語言模型)。通過將多個鏈式與其他組件集成,可以生成更復雜的鏈式結構。

LLMChain 被認爲是查詢 LLM 對象最常用的方法之一。 它根據提示模板將提供的輸入鍵值和內存鍵值(如果存在)進行格式化,然後將格式化後的字符串發送給 LLM,LLM 生成並返回輸出結果。

在調用語言模型後,可以按照一系列步驟進行操作,可以進行多個模型調用的序列。這種做法特別有價值,當希望將一個調用的輸出作爲另一個調用的輸入時。在這個鏈式序列中,每個鏈式都有一個輸入和一個輸出,一個步驟的輸出作爲下一個步驟的輸入。

#Here we are chaining everything
from langchain.chat_models import ChatOpenAI
from langchain.prompts.chat import (
    ChatPromptTemplate,
    HumanMessagePromptTemplate,
)
human_message_prompt = HumanMessagePromptTemplate(
        prompt=PromptTemplate(
            template="What is a good name for a company that makes {product}?",
            input_variables=["product"],
        )
    )
chat_prompt_template = ChatPromptTemplate.from_messages([human_message_prompt])
chat = ChatOpenAI(temperature=0.9)
# Temperature is about randomness in answer more the temp, random the answer
#Final Chain

chain = LLMChain(llm=chat, prompt=chat_prompt_template)
print(chain.run("colorful socks"))

Agent

某些應用可能需要不僅預定的 LLM(大型語言模型)/ 其他工具調用順序,還可能需要根據用戶的輸入確定不確定的調用順序。這種情況下涉及到的序列包括一個 “代理(Agent)”,該代理可以訪問多種工具。根據用戶的輸入,代理可能決定是否調用這些工具,並確定調用時的輸入。

根據文檔,代理的高級僞代碼大致如下:

  1. 接收用戶輸入。

  2. 代理根據輸入決定是否使用某個工具,以及該工具的輸入應該是什麼。

  3. 調用該工具,並記錄觀測結果(即使用該工具和輸入調用後得到的輸出)。

  4. 將工具、工具輸入和觀測結果的歷史傳遞迴代理,代理決定下一步應該採取的步驟。

  5. 重複上述步驟,直到代理決定不再需要使用工具,然後直接向用戶作出迴應。

這個過程會一直重複,直到代理決定不再需要使用工具,然後直接回應用戶。

案例:

from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.llms import OpenAI

llm = OpenAI(temperature=0)

tools = load_tools(["serpapi", "llm-math"], llm=llm)

agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)

agent.run("Who is Leo DiCaprio's girlfriend? What is her current age raised to the 0.43 power?")

讓我們將所有內容總結在下面這張圖中。

理解所有模塊和鏈式操作對於使用 LangChain 構建大語言模型的管道應用程序非常重要。這只是對 LangChain 的簡單介紹。

02 LangChain 的實際應用

廢話少說,讓我們直接使用 LangChain 構建簡單的應用程序。其中最有趣的應用是在自定義數據上創建一個問答機器人。

免責聲明 / 警告:這段代碼僅用於展示應用程序的構建方式。我並不保證代碼的優化,並且根據具體的問題陳述,可能需要進行進一步改進。

開始導入模塊

導入 LangChain 和 OpenAI 用於大語言模型部分。如果你還沒有安裝它們,請先安裝。

#    IMPORTS
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import CharacterTextSplitter
from langchain.chains import ConversationalRetrievalChain
from langchain.vectorstores import ElasticVectorSearch, Pinecone, Weaviate, FAISS
from PyPDF2 import PdfReader
from langchain import OpenAI, VectorDBQA
from langchain.vectorstores import Chroma
from langchain.prompts import PromptTemplate
from langchain.chains import ConversationChain

from langchain.document_loaders import TextLoader
# from langchain import ConversationalRetrievalChain
from langchain.chains.question_answering import load_qa_chain
from langchain import LLMChain
# from langchain import retrievers
import langchain
from langchain.chains.conversation.memory import ConversationBufferMemory

py2PDF 是用於讀取和處理 PDF 文件的工具。此外,還有不同類型的內存,例如 ConversationBufferMemory 和 ConversationBufferWindowMemory,它們具有特定的功能。我將在最後一部分詳細介紹有關內存的內容。

設置環境

我想你知道如何獲取 OpenAI API 密鑰,但是我還是想說明一下:

  1. 前往 OpenAI API 頁面,

  2. 點擊 “Create new secret key(創建新的密鑰)”

  3. 那將是你的 API 密鑰。請將其粘貼在下方

import os  
os.environ["OPENAI_API_KEY"] = "sk-YOUR API KEY"

要使用哪個模型?Davinci、Babbage、Curie 還是 Ada?基於 GPT-3、基於 GPT-3.5、還是基於 GPT-4?關於模型有很多問題,所有模型都適用於不同的任務。有些模型價格較低,有些模型更準確。

爲了簡單起見,我們將使用最經濟實惠的模型 “gpt-3.5-turbo”。溫度是一個參數,它影響答案的隨機性。溫度值越高,我們得到的答案就越隨機。

llm = ChatOpenAI(temperature=0,model_)

在這裏,你可以添加自己的數據。你可以使用任何格式,如 PDF、文本、文檔或 CSV。根據你的數據格式,你可以取消 / 註釋以下代碼。

# Custom data
from langchain.document_loaders import DirectoryLoader
pdf_loader = PdfReader(r'Your PDF location')

# excel_loader = DirectoryLoader('./Reports/', glob="**/*.txt")
# word_loader = DirectoryLoader('./Reports/', glob="**/*.docx")

我們無法一次性添加所有數據。我們將數據分成塊並將其發送以創建數據 Embedding。

Embedding 是以數字向量或數組的形式表示的,它們捕捉了模型處理和生成的標記的實質和上下文信息。這些嵌入是通過模型的參數或權重派生出來的,用於編碼和解碼輸入和輸出文本。

這就是 Embedding 的創建方式。

簡單來說,在 LLM 中,Embedding 是將文本表示爲數字向量的一種方式。這使得語言模型能夠理解單詞和短語的含義,並執行文本分類、摘要和翻譯等任務。

通俗地說,Embedding 是將單詞轉化爲數字的一種方法。 這是通過在大量文本語料庫上訓練機器學習模型來實現的。模型學會將每個單詞與一個唯一的數字向量相關聯。該向量代表單詞的含義,以及與其他單詞的關係。

讓我們做與上圖所示完全相同的事情。

#Preprocessing of file

raw_text = ''
for i, page in enumerate(pdf_loader.pages):
    text = page.extract_text()
    if text:
        raw_text += text

# print(raw_text[:100])


text_splitter = CharacterTextSplitter(        
    separator = "\n",
    chunk_size = 1000,
    chunk_overlap  = 200,
    length_function = len,
)
texts = text_splitter.split_text(raw_text)

在實際情況中,當用戶發起查詢時,將在向量存儲中進行搜索,並檢索出最合適的索引,然後將其傳遞給 LLM。然後,LLM 會重新構建索引中的內容,以向用戶提供格式化的響應。

我建議進一步深入研究向量存儲和 Embedding 的概念,以增強你的理解。

embeddings = OpenAIEmbeddings()
# vectorstore = Chroma.from_documents(documents, embeddings)
vectorstore = FAISS.from_texts(texts, embeddings)

嵌入向量直接存儲在一個向量數據庫中。有許多可用的向量數據庫,如 Pinecone、FAISS 等。在這裏,我們將使用 FAISS。

prompt_template = """Use the following pieces of context to answer the question at the end. If you don't know the answer, just say GTGTGTGTGTGTGTGTGTG, don't try to make up an answer.
{context}
Question: {question}
Helpful Answer:"""
QA_PROMPT = PromptTemplate(
    template=prompt_template, input_variables=['context',"question"]
)

你可以使用自己的提示來優化查詢和答案。在編寫提示後,讓我們將其與最終的鏈條進行鏈接。

讓我們調用最後的鏈條,其中包括之前鏈接的所有內容。我們在這裏使用 ConversationalRetrievalChain。它可以幫助我們以人類的方式進行對話,並記住之前的聊天記錄。

qa = ConversationalRetrievalChain.from_llm(ChatOpenAI(temperature=0.8), vectorstore.as_retriever(),qa_prompt=QA_PROMPT)

我們將使用簡單的 Gradio 來創建一個 Web 應用。你可以選擇使用 Streamlit 或其他前端技術。此外,還有許多免費的部署選項可供選擇,例如部署到 Hugging Face 或本地主機上,我們可以在稍後進行。

# Front end web app
import gradio as gr
with gr.Blocks() as demo:
    gr.Markdown("## Grounding DINO ChatBot")
    chatbot = gr.Chatbot()
    msg = gr.Textbox()
    clear = gr.Button("Clear")
    chat_history = []
  def user(user_message, history)
        print("Type of use msg:",type(user_message))
        # Get response from QA chain
        response = qa({"question": user_message, "chat_history": history})
        # Append user message and response to chat history
        history.append((user_message, response["answer"]))
        print(history)
        return gr.update(value=""), history
    msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False)
    clear.click(lambda: None, None, chatbot, queue=False)
    ############################################

if __name__ == "__main__":
    demo.launch(debug=True)

這段代碼將在本地創建一個鏈接,你可以直接提出問題並查看回答。同時,在你的集成開發環境(IDE)中,你將看到聊天曆史記錄的維護情況。

LangChain 快照

這是一個簡單的介紹,展示瞭如何通過連接不同的模塊來創建最終的鏈條。通過對模塊和代碼進行調整,你可以實現許多不同的功能。我想說,玩耍是研究的最高形式

03 LangChain 的 Token 和模型

Token

Token 可以被視爲單詞的組成部分。 在處理提示信息之前,API 會將輸入拆分成 Token。Token 的切分位置不一定與單詞的開始或結束位置完全對應,還可能包括尾隨的空格甚至子詞。

在自然語言處理中,我們通常會進行 Tokenizer 的操作,將段落拆分爲句子或單詞。在這裏,我們也將句子和段落切分成由單詞組成的小塊。

上圖顯示瞭如何將文本分割爲 Token。不同顏色表示不同的 Token。一個經驗法則是,一個 Token 大約相當於常見英語文本中的 4 個字符。這意味着 100 個 Token 大約相當於 75 個單詞。

如果你想檢查特定文本的 Token 數量,可以直接在 OpenAI 的 Tokenizer 上進行檢查。

另一種計算 Token 數量的方法是使用 tiktoken 庫。

import tiktoken
#Write function to take string input and return number of tokens 
def num_tokens_from_string(string: str, encoding_name: str) -> int:
    """Returns the number of tokens in a text string."""
    encoding = tiktoken.encoding_for_model(encoding_name)
    num_tokens = len(encoding.encode(string))
    return num_tokens

最後,使用上述函數:

prompt = []
for i in data:
    prompt.append((num_tokens_from_string(i['prompt'], "davinci")))
    
completion = []
for j in data:
    completion.append((num_tokens_from_string(j['completion'], "davinci")))
    
res_list = []
for i in range(0, len(prompt)):
    res_list.append(prompt[i] + completion[i])
    
no_of_final_token = 0
for i in res_list:
    no_of_final_token+=i
print("Number of final token",no_of_final_token)

輸出:

Number of final token 2094

不同模型的選擇受到 Token 數量的影響。

首先,讓我們瞭解 OpenAI 提供的不同模型。在本博客中,我專注於 OpenAI 模型。我們也可以使用 hugging faces 和 cohere AI 模型。

讓我們先了解基本模型。

Model

GPT 之所以強大,是因爲它是在大規模數據集上進行訓練的。 然而,強大的功能也伴隨着一定的代價,因此 OpenAI 提供了多個可供選擇的模型,也被稱爲引擎。

Davinci 是最大、功能最強大的引擎。它可以執行其他引擎可以執行的所有任務。Babbage 是次強大的引擎,它可以執行 Curie 和 Ada 能夠執行的任務。Ada 是功能最弱的引擎,但它性能最佳且價格最低。

隨着 GPT 的不斷髮展,還有許多不同版本的模型可供選擇。GPT 系列中大約有 50 多個模型可供使用。

截圖自 OpenAI 官方模型頁面

因此,針對不同的用途,有不同的模型可供選擇,包括生成和編輯圖像、處理音頻和編碼等。對於文本處理和自然語言處理,我們希望選擇能夠準確執行任務的模型。在上圖中,我們可以看到三個可用的模型:

然而,目前我們無法直接使用 GPT-4,因爲 GPT-4 目前僅限於有限的測試階段,只有特定授權用戶才能使用。我們需要加入等待列表並等待授權。因此,現在我們只剩下兩個選擇,即 GPT-3 和 GPT-3.5。

截圖自 OpenAI 官方模型頁面

上圖展示了 GPT-3 和 GPT-3.5 可用的模型。你可以看到這些模型都是基於 Davinci、Babbage、Curie 和 Ada 的不同版本。

如果你觀察上面的圖表,會發現有一個名爲 “Max tokens” 的列。“Max tokens” 是 OpenAI 模型的一個參數,用於限制單個請求中可以生成的 Token 數量。該限制包括提示和完成的 Token 數量。

換句話說,如果你的提示佔用了 1,000 個 Token,那麼你只能生成最多 3,000 個 Token 的完成文本。此外,“Max tokens” 限制由 OpenAI 服務器執行。如果你嘗試生成超過限制的文本,你的請求將被拒絕。

基於 GPT-3 的模型具有較低的 “Max tokens” 數值(2049),而基於 GPT-3.5 的模型具有較高的數值(4096)。因此,使用 GPT-3。5 模型可以處理更多的數據量。

接下來,讓我們來了解不同模型的定價。

我們可以選擇基於 GPT-3.5 的 “gpt-3.5-turbo” 模型。

假設我有 5000 個單詞,並且我使用 “gpt-3.5-turbo” 模型,那麼:

5000 個單詞約等於 6667 個 Token。

現在,對於 1000 個 Token,我們需要 0.002 美元。

因此,對於 6667 個 Token,我們大約需要 0.0133 美元。

我們可以粗略計算需要多少使用量來進行處理。同時,迭代次數是會改變 Token 數量的一個參數,因此在計算中需要考慮這一點。

現在,你可以理解 Token 的重要性了吧。這就是爲什麼我們必須進行非常乾淨和適當的預處理,以減少文檔中的噪聲,同時也減少處理 Token 的成本。因此,正確清理文本非常重要,例如消除噪聲。甚至刪除多餘的空格也可以爲你的 API 密鑰節省費用。

讓我們在一個內存圖中查看所有模型。

總結

Token 對於問題回答或其他 LLM 相關任務至關重要。如何以一種能夠使用更便宜的模型的方式預處理數據是真正的變革因素。模型的選擇取決於你希望做出的權衡。Davinci 系列將以更高的速度和準確性提供服務,但成本較高。而基於 GPT-3.5 Turbo 的模型將節省費用,但速度較慢。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/BvPBVN1BtxuQrqATDjAJvw