一、LangChain是什么
LangChain是一個框架,用于開發(fā)由LLM驅(qū)動的應(yīng)用程序。可以簡單認為是LLM領(lǐng)域的Spring,以及開源版的ChatGPT插件系統(tǒng)。核心的2個功能為:
1)可以將 LLM 模型與外部數(shù)據(jù)源進行連接。
2)允許與 LLM 模型與環(huán)境進行交互,通過Agent使用工具。
二、LangChain核心組件
LangChain提供了各種不同的組件幫助使用LLM,如下圖所示,核心組件有Models、Indexes、Chains、Memory以及Agent。
??2.1 Models
LangChain本身不提供LLM,提供通用的接口訪問LLM,可以很方便的更換底層的LLM以及自定義自己的LLM。主要有2大類的Models:
1)LLM:將文本字符串作為輸入并返回文本字符串的模型,類似OpenAI的text-davinci-003
2)Chat Models:由語言模型支持但將聊天消息列表作為輸入并返回聊天消息的模型。一般使用的ChatGPT以及Claude為Chat Models。
與模型交互的,基本上是通過給予Prompt的方式,LangChain通過PromptTemplate的方式方便我們構(gòu)建以及復(fù)用Prompt。
from langchain import PromptTemplate prompt_template = '''作為一個資深編輯,請針對 >>> 和 <<< 中間的文本寫一段摘要。 >>> {text} <<< ''' prompt = PromptTemplate(template=prompt_template, input_variables=["text"]) print(prompt.format_prompt(text="我愛北京天安門"))
2.2 Indexes
索引和外部數(shù)據(jù)進行集成,用于從外部數(shù)據(jù)獲取答案。如下圖所示,主要的步驟有
1)通過Document Loaders加載各種不同類型的數(shù)據(jù)源,
2)通過Text Splitters進行文本語義分割
3)通過Vectorstore進行非結(jié)構(gòu)化數(shù)據(jù)的向量存儲
4)通過Retriever進行文檔數(shù)據(jù)檢索
?
2.2.1 Document Loaders
LangChain通過Loader加載外部的文檔,轉(zhuǎn)化為標準的Document類型。Document類型主要包含兩個屬性:page_content 包含該文檔的內(nèi)容。meta_data 為文檔相關(guān)的描述性數(shù)據(jù),類似文檔所在的路徑等。
??2.2.2 Text Splitters
LLM一般都會限制上下文窗口的大小,有4k、16k、32k等。針對大文本就需要進行文本分割,常用的文本分割器為RecursiveCharacterTextSplitter,可以通過separators指定分隔符。其先通過第一個分隔符進行分割,不滿足大小的情況下迭代分割。
文本分割主要有2個考慮:
1)將語義相關(guān)的句子放在一塊形成一個chunk。一般根據(jù)不同的文檔類型定義不同的分隔符,或者可以選擇通過模型進行分割。
2)chunk控制在一定的大小,可以通過函數(shù)去計算。默認通過len函數(shù)計算,模型內(nèi)部一般都是使用token進行計算。token通常指的是將文本或序列數(shù)據(jù)劃分成的小的單元或符號,便于機器理解和處理。使用OpenAI相關(guān)的大模型,可以通過tiktoken包去計算其token大小。
?
?
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( model_name="gpt-3.5-turb allowed_special="all", separators=[" ", " ", "。", ","], chunk_size=7000, chunk_overlap=0 ) docs = text_splitter.create_documents(["文本在這里"]) print(docs)
?
?
2.2.3 Vectorstore
通過Text Embedding models,將文本轉(zhuǎn)為向量,可以進行語義搜索,在向量空間中找到最相似的文本片段。目前支持常用的向量存儲有Faiss、Chroma等。
Embedding模型支持OpenAIEmbeddings、HuggingFaceEmbeddings等。通過HuggingFaceEmbeddings加載本地模型可以節(jié)省embedding的調(diào)用費用。
?
?
#通過cache_folder加載本地模型 embeddings = HuggingFaceEmbeddings(model_name="text2vec-base-chinese", cache_folder="本地模型地址") embeddings = embeddings_model.embed_documents( [ "我愛北京天安門!", "Hello world!" ] )
?
?
2.2.4 Retriever
Retriever接口用于根據(jù)非結(jié)構(gòu)化的查詢獲取文檔,一般情況下是文檔存儲在向量數(shù)據(jù)庫中??梢哉{(diào)用 get_relevant_documents 方法來檢索與查詢相關(guān)的文檔。
?
?
from langchain import FAISS from langchain.document_loaders import WebBaseLoader from langchain.embeddings import HuggingFaceEmbeddings from langchain.text_splitter import RecursiveCharacterTextSplitter loader = WebBaseLoader("https://in.m.jd.com/help/app/register_info.html") data = loader.load() text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( model_name="gpt-3.5-turbo", allowed_special="all", separators=[" ", " ", "。", ","], chunk_size=800, chunk_overlap=0 ) docs = text_splitter.split_documents(data) #通過cache_folder設(shè)置自己的本地模型路徑 embeddings = HuggingFaceEmbeddings(model_name="text2vec-base-chinese", cache_folder="models") vectorstore = FAISS.from_documents(docs, embeddings) result = vectorstore.as_retriever().get_relevant_documents("用戶注冊資格") print(result) print(len(result))
?
?
2.3 Chains
Langchain通過chain將各個組件進行鏈接,以及chain之間進行鏈接,用于簡化復(fù)雜應(yīng)用程序的實現(xiàn)。其中主要有LLMChain、Sequential Chain以及Route Chain
2.3.1 LLMChain
最基本的鏈為LLMChain,由PromptTemplate、LLM和OutputParser組成。LLM的輸出一般為文本,OutputParser用于讓LLM結(jié)構(gòu)化輸出并進行結(jié)果解析,方便后續(xù)的調(diào)用。
類似下面的示例,給評論進行關(guān)鍵詞提前以及情緒分析,通過LLMChain組合PromptTemplate、LLM以及OutputParser,可以很簡單的實現(xiàn)一個之前通過依賴小模型不斷需要調(diào)優(yōu)的事情。
?
?
from langchain.chains import LLMChain from langchain.prompts import PromptTemplate from langchain.output_parsers import ResponseSchema, StructuredOutputParser from azure_chat_llm import llm #output parser keyword_schema = ResponseSchema(name="keyword", description="評論的關(guān)鍵詞列表") emotion_schema = ResponseSchema(name="emotion", description="評論的情緒,正向為1,中性為0,負向為-1") response_schemas = [keyword_schema, emotion_schema] output_parser = StructuredOutputParser.from_response_schemas(response_schemas) format_instructions = output_parser.get_format_instructions() #prompt template prompt_template_txt = ''' 作為資深客服,請針對 >>> 和 <<< 中間的文本識別其中的關(guān)鍵詞,以及包含的情緒是正向、負向還是中性。 >>> {text} <<< RESPONSE: {format_instructions} ''' prompt = PromptTemplate(template=prompt_template_txt, input_variables=["text"], partial_variables={"format_instructions": format_instructions}) #llmchain llm_chain = LLMChain(prompt=prompt, llm=llm) comment = "京東物流沒的說,速度態(tài)度都是杠杠滴!這款路由器顏值賊高,怎么說呢,就是泰褲辣!這線條,這質(zhì)感,這速度,嘎嘎快!以后媽媽再也不用擔心家里的網(wǎng)速了!" result = llm_chain.run(comment) data = output_parser.parse(result) print(f"type={type(data)}, keyword={data['keyword']}, emotion={data['emotion']}")
?
?
輸出:
?
2.3.2 Sequential Chain
SequentialChains是按預(yù)定義順序執(zhí)行的鏈。SimpleSequentialChain為順序鏈的最簡單形式,其中每個步驟都有一個單一的輸入/輸出,一個步驟的輸出是下一個步驟的輸入。SequentialChain 為順序鏈更通用的形式,允許多個輸入/輸出。
?
?
from langchain.chains import LLMChain from langchain.prompts import PromptTemplate from langchain.chains import SimpleSequentialChain first_prompt = PromptTemplate.from_template( "翻譯下面的內(nèi)容到中文:" " {content}" ) # chain 1: 輸入:Review 輸出: 英文的 Review chain_trans = LLMChain(llm=llm, prompt=first_prompt, output_key="content_zh") second_prompt = PromptTemplate.from_template( "一句話總結(jié)下面的內(nèi)容:" " {content_zh}" ) chain_summary = LLMChain(llm=llm, prompt=second_prompt) overall_simple_chain = SimpleSequentialChain(chains=[chain_trans, chain_summary],verbose=True) content = '''In a blog post authored back in 2011, Marc Andreessen warned that, “Software is eating the world.” Over a decade later, we are witnessing the emergence of a new type of technology that’s consuming the world with even greater voracity: generative artificial intelligence (AI). This innovative AI includes a unique class of large language models (LLM), derived from a decade of groundbreaking research, that are capable of out-performing humans at certain tasks. And you don’t have to have a PhD in machine learning to build with LLMs—developers are already building software with LLMs with basic HTTP requests and natural language prompts. In this article, we’ll tell the story of GitHub’s work with LLMs to help other developers learn how to best make use of this technology. This post consists of two main sections: the first will describe at a high level how LLMs function and how to build LLM-based applications. The second will dig into an important example of an LLM-based application: GitHub Copilot code completions. Others have done an impressive job of cataloging our work from the outside. Now, we’re excited to share some of the thought processes that have led to the ongoing success of GitHub Copilot. ''' result = overall_simple_chain.run(content) print(f'result={result}')
?
?
輸出:
?
2.3.3 Router Chain
RouterChain是根據(jù)輸入動態(tài)的選擇下一個鏈,每條鏈處理特定類型的輸入。
RouterChain由兩個組件組成:
1)路由器鏈本身,負責選擇要調(diào)用的下一個鏈,主要有2種RouterChain,其中LLMRouterChain通過LLM進行路由決策,EmbeddingRouterChain 通過向量搜索的方式進行路由決策。
2)目標鏈列表,路由器鏈可以路由到的子鏈。
初始化RouterChain以及destination_chains完成后,通過MultiPromptChain將兩者結(jié)合起來使用。
??2.3.4 Documents Chain
下面的4種Chain主要用于Document的處理,在基于文檔生成摘要、基于文檔的問答等場景中經(jīng)常會用到,在后續(xù)的落地實踐里也會有所體現(xiàn)。
2.3.4.1 Stuff
StuffDocumentsChain這種鏈最簡單直接,是將所有獲取到的文檔作為context放入到Prompt中,傳遞到LLM獲取答案。
這種方式可以完整的保留上下文,調(diào)用LLM的次數(shù)也比較少,建議能使用stuff的就使用這種方式。其適合文檔拆分的比較小,一次獲取文檔比較少的場景,不然容易超過token的限制。
??2.3.4.2 Refine
RefineDocumentsChain是通過迭代更新的方式獲取答案。先處理第一個文檔,作為context傳遞給llm,獲取中間結(jié)果intermediate answer。然后將第一個文檔的中間結(jié)果以及第二個文檔發(fā)給llm進行處理,后續(xù)的文檔類似處理。
Refine這種方式能部分保留上下文,以及token的使用能控制在一定范圍。
??2.3.4.3 MapReduce
MapReduceDocumentsChain先通過LLM對每個document進行處理,然后將所有文檔的答案在通過LLM進行合并處理,得到最終的結(jié)果。
MapReduce的方式將每個document單獨處理,可以并發(fā)進行調(diào)用。但是每個文檔之間缺少上下文。
?
2.3.4.4 MapRerank
MapRerankDocumentsChain和MapReduceDocumentsChain類似,先通過LLM對每個document進行處理,每個答案都會返回一個score,最后選擇score最高的答案。
MapRerank和MapReduce類似,會大批量地調(diào)用LLM,每個document之間是獨立處理。
??2.4 Memory
正常情況下Chain無狀態(tài)的,每次交互都是獨立的,無法知道之前歷史交互的信息。LangChain使用Memory組件保存和管理歷史消息,這樣可以跨多輪進行對話,在當前會話中保留歷史會話的上下文。Memory組件支持多種存儲介質(zhì),可以與Monogo、Redis、SQLite等進行集成,以及簡單直接形式就是Buffer Memory。常用的Buffer Memory有
1)ConversationSummaryMemory :以摘要的信息保存記錄
2)ConversationBufferWindowMemory:以原始形式保存最新的n條記錄
3)ConversationBufferMemory:以原始形式保存所有記錄
通過查看chain的prompt,可以發(fā)現(xiàn){history}變量傳遞了從memory獲取的會話上下文。下面的示例演示了Memory的使用方式,可以很明細看到,答案是從之前的問題里獲取的。
?
?
from langchain.chains import ConversationChain from langchain.memory import ConversationBufferMemory from azure_chat_llm import llm memory = ConversationBufferMemory() conversation = ConversationChain(llm=llm, memory=memory, verbose=True) print(conversation.prompt) print(conversation.predict(input="我的姓名是tiger")) print(conversation.predict(input="1+1=?")) print(conversation.predict(input="我的姓名是什么"))
?
?
輸出:
??2.5 Agent
Agent字面含義就是代理,如果說LLM是大腦,Agent就是代理大腦使用工具Tools。目前的大模型一般都存在知識過時、邏輯計算能力低等問題,通過Agent訪問工具,可以去解決這些問題。目前這個領(lǐng)域特別活躍,誕生了類似AutoGPT、BabyAGI、AgentGPT等一堆優(yōu)秀的項目。傳統(tǒng)使用LLM,需要給定Prompt一步一步地達成目標,通過Agent是給定目標,其會自動規(guī)劃并達到目標。
2.5.1 Agent核心組件
Agent:代理,負責調(diào)用LLM以及決定下一步的Action。其中LLM的prompt必須包含agent_scratchpad變量,記錄執(zhí)行的中間過程
Tools:工具,Agent可以調(diào)用的方法。LangChain已有很多內(nèi)置的工具,也可以自定義工具。注意Tools的description屬性,LLM會通過描述決定是否使用該工具。
ToolKits:工具集,為特定目的的工具集合。類似Office365、Gmail工具集等
Agent Executor:Agent執(zhí)行器,負責進行實際的執(zhí)行。
2.5.2 Agent的類型
一般通過initialize_agent函數(shù)進行Agent的初始化,除了llm、tools等參數(shù),還需要指定AgentType。
?
?
agent = initialize_agent(agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, tools=tools, llm=llm, verbose=True) print(agent.agent.llm_chain.prompt.template)
?
?
該Agent為一個zero-shot-react-description類型的Agent,其中zero-shot表明只考慮當前的操作,不會記錄以及參考之前的操作。react表明通過ReAct框架進行推理,description表明通過工具的description進行是否使用的決策。
其他的類型還有chat-conversational-react-description、conversational-react-description、react-docstore、self-ask-with-search等,類似chat-conversational-react-description通過memory記錄之前的對話,應(yīng)答會參考之前的操作。
可以通過agent.agent.llm_chain.prompt.template方法,獲取其推理決策所使用的模板。
2.5.3 自定義Tool
有多種方式可以自定義Tool,最簡單的方式是通過@tool裝飾器,將一個函數(shù)轉(zhuǎn)為Tool。注意函數(shù)必須得有docString,其為Tool的描述。
?
?
from azure_chat_llm import llm from langchain.agents import load_tools, initialize_agent, tool from langchain.agents.agent_types import AgentType from datetime import date @tool def time(text: str) -> str: """ 返回今天的日期。 """ return str(date.today()) tools = load_tools(['llm-math'], llm=llm) tools.append(time) agent_math = initialize_agent(agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, tools=tools, llm=llm, verbose=True) print(agent_math("計算45 * 54")) print(agent_math("今天是哪天?"))
?
?
輸出為:
三、LangChain落地實踐
3.1 文檔生成總結(jié)
1)通過Loader加載遠程文檔
2)通過Splitter基于Token進行文檔拆分
3)加載summarize鏈,鏈類型為refine,迭代進行總結(jié)
?
?
from langchain.prompts import PromptTemplate from langchain.document_loaders import PlaywrightURLLoader from langchain.chains.summarize import load_summarize_chain from langchain.text_splitter import RecursiveCharacterTextSplitter from azure_chat_llm import llm loader = PlaywrightURLLoader(urls=["https://content.jr.jd.com/article/index.html?pageId=708258989"]) data = loader.load() text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( model_name="gpt-3.5-turbo", allowed_special="all", separators=[" ", " ", "。", ","], chunk_size=7000, chunk_overlap=0 ) prompt_template = ''' 作為一個資深編輯,請針對 >>> 和 <<< 中間的文本寫一段摘要。 >>> {text} <<< ''' refine_template = ''' 作為一個資深編輯,基于已有的一段摘要:{existing_answer},針對 >>> 和 <<< 中間的文本完善現(xiàn)有的摘要。 >>> {text} <<< ''' PROMPT = PromptTemplate(template=prompt_template, input_variables=["text"]) REFINE_PROMPT = PromptTemplate( template=refine_template, input_variables=["existing_answer", "text"] ) chain = load_summarize_chain(llm, chain_type="refine", question_prompt=PROMPT, refine_prompt=REFINE_PROMPT, verbose=False) docs = text_splitter.split_documents(data) result = chain.run(docs) print(result)
?
?
3.2 基于外部文檔的問答
1)通過Loader加載遠程文檔
2)通過Splitter基于Token進行文檔拆分
3)通過FAISS向量存儲文檔,embedding加載HuggingFace的text2vec-base-chinese模型
4)自定義QA的prompt,通過RetrievalQA回答相關(guān)的問題
?
?
from langchain.chains import RetrievalQA from langchain.document_loaders import WebBaseLoader from langchain.embeddings.huggingface import HuggingFaceEmbeddings from langchain.prompts import PromptTemplate from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.vectorstores import FAISS from azure_chat_llm import llm loader = WebBaseLoader("https://in.m.jd.com/help/app/register_info.html") data = loader.load() text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( model_name="gpt-3.5-turbo", allowed_special="all", separators=[" ", " ", "。", ","], chunk_size=800, chunk_overlap=0 ) docs = text_splitter.split_documents(data) #設(shè)置自己的模型路徑 embeddings = HuggingFaceEmbeddings(model_name="text2vec-base-chinese", cache_folder="model") vectorstore = FAISS.from_documents(docs, embeddings) template = """請使用下面提供的背景信息來回答最后的問題。 如果你不知道答案,請直接說不知道,不要試圖憑空編造答案。 回答時最多使用三個句子,保持回答盡可能簡潔。 回答結(jié)束時,請一定要說"謝謝你的提問!" {context} 問題: {question} 有用的回答:""" QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context", "question"], template=template) qa_chain = RetrievalQA.from_chain_type(llm, retriever=vectorstore.as_retriever(), return_source_documents=True, chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}) result = qa_chain({"query": "用戶注冊資格"}) print(result["result"]) print(len(result['source_documents']))? ? 四、未來發(fā)展方向
?
?
隨著大模型的發(fā)展,LangChain應(yīng)該是目前最火的LLM開發(fā)框架,能和外部數(shù)據(jù)源交互、能集成各種常用的組件等等,大大降低了LLM應(yīng)用開發(fā)的門檻。其創(chuàng)始人Harrison Chase也和Andrew Ng聯(lián)合開發(fā)了2門短課程,幫忙大家快速掌握LangChain的使用。
目前大模型的迭代升級特別快,作為一個框架,LangChain也得保持特別快的迭代速度。其開發(fā)特別拼,每天都會提交大量的commit,基本隔幾天就會發(fā)布一個新版本,其Contributor也達到了1200多人,特別活躍。
個人認為,除了和業(yè)務(wù)結(jié)合落地LLM應(yīng)用外,還有2個大的方向可以進一步去探索:
1)通過低代碼的形式進一步降低LLM應(yīng)用的開發(fā)門檻。類似langflow這樣的可視化編排工具發(fā)展也很快
2)打造更加強大的Agent。Agent之于大模型,個人覺得類似SQL之于DB,能大幅度提升LLM的應(yīng)用場景
審核編輯:黃飛
?
評論
查看更多