一、LangChain是什么
LangChain是一個(gè)框架,用于開(kāi)發(fā)由LLM驅(qū)動(dòng)的應(yīng)用程序??梢院?jiǎn)單認(rèn)為是LLM領(lǐng)域的Spring,以及開(kāi)源版的ChatGPT插件系統(tǒng)。核心的2個(gè)功能為:
1)可以將 LLM 模型與外部數(shù)據(jù)源進(jìn)行連接。
2)允許與 LLM 模型與環(huán)境進(jìn)行交互,通過(guò)Agent使用工具。
二、LangChain核心組件
LangChain提供了各種不同的組件幫助使用LLM,如下圖所示,核心組件有Models、Indexes、Chains、Memory以及Agent。
??2.1 Models
LangChain本身不提供LLM,提供通用的接口訪問(wèn)LLM,可以很方便的更換底層的LLM以及自定義自己的LLM。主要有2大類的Models:
1)LLM:將文本字符串作為輸入并返回文本字符串的模型,類似OpenAI的text-davinci-003
2)Chat Models:由語(yǔ)言模型支持但將聊天消息列表作為輸入并返回聊天消息的模型。一般使用的ChatGPT以及Claude為Chat Models。
與模型交互的,基本上是通過(guò)給予Prompt的方式,LangChain通過(guò)PromptTemplate的方式方便我們構(gòu)建以及復(fù)用Prompt。
from langchain import PromptTemplate prompt_template = '''作為一個(gè)資深編輯,請(qǐng)針對(duì) >>> 和 <<< 中間的文本寫(xiě)一段摘要。 >>> {text} <<< ''' prompt = PromptTemplate(template=prompt_template, input_variables=["text"]) print(prompt.format_prompt(text="我愛(ài)北京天安門(mén)"))
2.2 Indexes
索引和外部數(shù)據(jù)進(jìn)行集成,用于從外部數(shù)據(jù)獲取答案。如下圖所示,主要的步驟有
1)通過(guò)Document Loaders加載各種不同類型的數(shù)據(jù)源,
2)通過(guò)Text Splitters進(jìn)行文本語(yǔ)義分割
3)通過(guò)Vectorstore進(jìn)行非結(jié)構(gòu)化數(shù)據(jù)的向量存儲(chǔ)
4)通過(guò)Retriever進(jìn)行文檔數(shù)據(jù)檢索
?
2.2.1 Document Loaders
LangChain通過(guò)Loader加載外部的文檔,轉(zhuǎn)化為標(biāo)準(zhǔn)的Document類型。Document類型主要包含兩個(gè)屬性:page_content 包含該文檔的內(nèi)容。meta_data 為文檔相關(guān)的描述性數(shù)據(jù),類似文檔所在的路徑等。
??2.2.2 Text Splitters
LLM一般都會(huì)限制上下文窗口的大小,有4k、16k、32k等。針對(duì)大文本就需要進(jìn)行文本分割,常用的文本分割器為RecursiveCharacterTextSplitter,可以通過(guò)separators指定分隔符。其先通過(guò)第一個(gè)分隔符進(jìn)行分割,不滿足大小的情況下迭代分割。
文本分割主要有2個(gè)考慮:
1)將語(yǔ)義相關(guān)的句子放在一塊形成一個(gè)chunk。一般根據(jù)不同的文檔類型定義不同的分隔符,或者可以選擇通過(guò)模型進(jìn)行分割。
2)chunk控制在一定的大小,可以通過(guò)函數(shù)去計(jì)算。默認(rèn)通過(guò)len函數(shù)計(jì)算,模型內(nèi)部一般都是使用token進(jìn)行計(jì)算。token通常指的是將文本或序列數(shù)據(jù)劃分成的小的單元或符號(hào),便于機(jī)器理解和處理。使用OpenAI相關(guān)的大模型,可以通過(guò)tiktoken包去計(jì)算其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
通過(guò)Text Embedding models,將文本轉(zhuǎn)為向量,可以進(jìn)行語(yǔ)義搜索,在向量空間中找到最相似的文本片段。目前支持常用的向量存儲(chǔ)有Faiss、Chroma等。
Embedding模型支持OpenAIEmbeddings、HuggingFaceEmbeddings等。通過(guò)HuggingFaceEmbeddings加載本地模型可以節(jié)省embedding的調(diào)用費(fèi)用。
?
?
#通過(guò)cache_folder加載本地模型 embeddings = HuggingFaceEmbeddings(model_name="text2vec-base-chinese", cache_folder="本地模型地址") embeddings = embeddings_model.embed_documents( [ "我愛(ài)北京天安門(mén)!", "Hello world!" ] )
?
?
2.2.4 Retriever
Retriever接口用于根據(jù)非結(jié)構(gòu)化的查詢獲取文檔,一般情況下是文檔存儲(chǔ)在向量數(shù)據(jù)庫(kù)中??梢哉{(diào)用 get_relevant_documents 方法來(lái)檢索與查詢相關(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) #通過(guò)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("用戶注冊(cè)資格") print(result) print(len(result))
?
?
2.3 Chains
Langchain通過(guò)chain將各個(gè)組件進(jìn)行鏈接,以及chain之間進(jìn)行鏈接,用于簡(jiǎn)化復(fù)雜應(yīng)用程序的實(shí)現(xiàn)。其中主要有LLMChain、Sequential Chain以及Route Chain
2.3.1 LLMChain
最基本的鏈為L(zhǎng)LMChain,由PromptTemplate、LLM和OutputParser組成。LLM的輸出一般為文本,OutputParser用于讓LLM結(jié)構(gòu)化輸出并進(jìn)行結(jié)果解析,方便后續(xù)的調(diào)用。
類似下面的示例,給評(píng)論進(jìn)行關(guān)鍵詞提前以及情緒分析,通過(guò)LLMChain組合PromptTemplate、LLM以及OutputParser,可以很簡(jiǎn)單的實(shí)現(xiàn)一個(gè)之前通過(guò)依賴小模型不斷需要調(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="評(píng)論的關(guān)鍵詞列表") emotion_schema = ResponseSchema(name="emotion", description="評(píng)論的情緒,正向?yàn)?,中性為0,負(fù)向?yàn)?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 = ''' 作為資深客服,請(qǐng)針對(duì) >>> 和 <<< 中間的文本識(shí)別其中的關(guān)鍵詞,以及包含的情緒是正向、負(fù)向還是中性。 >>> {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 = "京東物流沒(méi)的說(shuō),速度態(tài)度都是杠杠滴!這款路由器顏值賊高,怎么說(shuō)呢,就是泰褲辣!這線條,這質(zhì)感,這速度,嘎嘎快!以后媽媽再也不用擔(dān)心家里的網(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為順序鏈的最簡(jiǎn)單形式,其中每個(gè)步驟都有一個(gè)單一的輸入/輸出,一個(gè)步驟的輸出是下一個(gè)步驟的輸入。SequentialChain 為順序鏈更通用的形式,允許多個(gè)輸入/輸出。
?
?
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ù)輸入動(dòng)態(tài)的選擇下一個(gè)鏈,每條鏈處理特定類型的輸入。
RouterChain由兩個(gè)組件組成:
1)路由器鏈本身,負(fù)責(zé)選擇要調(diào)用的下一個(gè)鏈,主要有2種RouterChain,其中LLMRouterChain通過(guò)LLM進(jìn)行路由決策,EmbeddingRouterChain 通過(guò)向量搜索的方式進(jìn)行路由決策。
2)目標(biāo)鏈列表,路由器鏈可以路由到的子鏈。
初始化RouterChain以及destination_chains完成后,通過(guò)MultiPromptChain將兩者結(jié)合起來(lái)使用。
??2.3.4 Documents Chain
下面的4種Chain主要用于Document的處理,在基于文檔生成摘要、基于文檔的問(wèn)答等場(chǎng)景中經(jīng)常會(huì)用到,在后續(xù)的落地實(shí)踐里也會(huì)有所體現(xiàn)。
2.3.4.1 Stuff
StuffDocumentsChain這種鏈最簡(jiǎn)單直接,是將所有獲取到的文檔作為context放入到Prompt中,傳遞到LLM獲取答案。
這種方式可以完整的保留上下文,調(diào)用LLM的次數(shù)也比較少,建議能使用stuff的就使用這種方式。其適合文檔拆分的比較小,一次獲取文檔比較少的場(chǎng)景,不然容易超過(guò)token的限制。
??2.3.4.2 Refine
RefineDocumentsChain是通過(guò)迭代更新的方式獲取答案。先處理第一個(gè)文檔,作為context傳遞給llm,獲取中間結(jié)果intermediate answer。然后將第一個(gè)文檔的中間結(jié)果以及第二個(gè)文檔發(fā)給llm進(jìn)行處理,后續(xù)的文檔類似處理。
Refine這種方式能部分保留上下文,以及token的使用能控制在一定范圍。
??2.3.4.3 MapReduce
MapReduceDocumentsChain先通過(guò)LLM對(duì)每個(gè)document進(jìn)行處理,然后將所有文檔的答案在通過(guò)LLM進(jìn)行合并處理,得到最終的結(jié)果。
MapReduce的方式將每個(gè)document單獨(dú)處理,可以并發(fā)進(jìn)行調(diào)用。但是每個(gè)文檔之間缺少上下文。
?
2.3.4.4 MapRerank
MapRerankDocumentsChain和MapReduceDocumentsChain類似,先通過(guò)LLM對(duì)每個(gè)document進(jìn)行處理,每個(gè)答案都會(huì)返回一個(gè)score,最后選擇score最高的答案。
MapRerank和MapReduce類似,會(huì)大批量地調(diào)用LLM,每個(gè)document之間是獨(dú)立處理。
??2.4 Memory
正常情況下Chain無(wú)狀態(tài)的,每次交互都是獨(dú)立的,無(wú)法知道之前歷史交互的信息。LangChain使用Memory組件保存和管理歷史消息,這樣可以跨多輪進(jìn)行對(duì)話,在當(dāng)前會(huì)話中保留歷史會(huì)話的上下文。Memory組件支持多種存儲(chǔ)介質(zhì),可以與Monogo、Redis、SQLite等進(jìn)行集成,以及簡(jiǎn)單直接形式就是Buffer Memory。常用的Buffer Memory有
1)ConversationSummaryMemory :以摘要的信息保存記錄
2)ConversationBufferWindowMemory:以原始形式保存最新的n條記錄
3)ConversationBufferMemory:以原始形式保存所有記錄
通過(guò)查看chain的prompt,可以發(fā)現(xiàn){history}變量傳遞了從memory獲取的會(huì)話上下文。下面的示例演示了Memory的使用方式,可以很明細(xì)看到,答案是從之前的問(wèn)題里獲取的。
?
?
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字面含義就是代理,如果說(shuō)LLM是大腦,Agent就是代理大腦使用工具Tools。目前的大模型一般都存在知識(shí)過(guò)時(shí)、邏輯計(jì)算能力低等問(wèn)題,通過(guò)Agent訪問(wèn)工具,可以去解決這些問(wèn)題。目前這個(gè)領(lǐng)域特別活躍,誕生了類似AutoGPT、BabyAGI、AgentGPT等一堆優(yōu)秀的項(xiàng)目。傳統(tǒng)使用LLM,需要給定Prompt一步一步地達(dá)成目標(biāo),通過(guò)Agent是給定目標(biāo),其會(huì)自動(dòng)規(guī)劃并達(dá)到目標(biāo)。
2.5.1 Agent核心組件
Agent:代理,負(fù)責(zé)調(diào)用LLM以及決定下一步的Action。其中LLM的prompt必須包含agent_scratchpad變量,記錄執(zhí)行的中間過(guò)程
Tools:工具,Agent可以調(diào)用的方法。LangChain已有很多內(nèi)置的工具,也可以自定義工具。注意Tools的description屬性,LLM會(huì)通過(guò)描述決定是否使用該工具。
ToolKits:工具集,為特定目的的工具集合。類似Office365、Gmail工具集等
Agent Executor:Agent執(zhí)行器,負(fù)責(zé)進(jìn)行實(shí)際的執(zhí)行。
2.5.2 Agent的類型
一般通過(guò)initialize_agent函數(shù)進(jìn)行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為一個(gè)zero-shot-react-description類型的Agent,其中zero-shot表明只考慮當(dāng)前的操作,不會(huì)記錄以及參考之前的操作。react表明通過(guò)ReAct框架進(jìn)行推理,description表明通過(guò)工具的description進(jìn)行是否使用的決策。
其他的類型還有chat-conversational-react-description、conversational-react-description、react-docstore、self-ask-with-search等,類似chat-conversational-react-description通過(guò)memory記錄之前的對(duì)話,應(yīng)答會(huì)參考之前的操作。
可以通過(guò)agent.agent.llm_chain.prompt.template方法,獲取其推理決策所使用的模板。
2.5.3 自定義Tool
有多種方式可以自定義Tool,最簡(jiǎn)單的方式是通過(guò)@tool裝飾器,將一個(gè)函數(shù)轉(zhuǎn)為T(mén)ool。注意函數(shù)必須得有docString,其為T(mén)ool的描述。
?
?
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("計(jì)算45 * 54")) print(agent_math("今天是哪天?"))
?
?
輸出為:
三、LangChain落地實(shí)踐
3.1 文檔生成總結(jié)
1)通過(guò)Loader加載遠(yuǎn)程文檔
2)通過(guò)Splitter基于Token進(jìn)行文檔拆分
3)加載summarize鏈,鏈類型為refine,迭代進(jìn)行總結(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 = ''' 作為一個(gè)資深編輯,請(qǐng)針對(duì) >>> 和 <<< 中間的文本寫(xiě)一段摘要。 >>> {text} <<< ''' refine_template = ''' 作為一個(gè)資深編輯,基于已有的一段摘要:{existing_answer},針對(duì) >>> 和 <<< 中間的文本完善現(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 基于外部文檔的問(wèn)答
1)通過(guò)Loader加載遠(yuǎn)程文檔
2)通過(guò)Splitter基于Token進(jìn)行文檔拆分
3)通過(guò)FAISS向量存儲(chǔ)文檔,embedding加載HuggingFace的text2vec-base-chinese模型
4)自定義QA的prompt,通過(guò)RetrievalQA回答相關(guān)的問(wè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 = """請(qǐng)使用下面提供的背景信息來(lái)回答最后的問(wèn)題。 如果你不知道答案,請(qǐng)直接說(shuō)不知道,不要試圖憑空編造答案。 回答時(shí)最多使用三個(gè)句子,保持回答盡可能簡(jiǎn)潔。 回答結(jié)束時(shí),請(qǐng)一定要說(shuō)"謝謝你的提問(wèn)!" {context} 問(wèn)題: {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": "用戶注冊(cè)資格"}) print(result["result"]) print(len(result['source_documents']))? ? 四、未來(lái)發(fā)展方向
?
?
隨著大模型的發(fā)展,LangChain應(yīng)該是目前最火的LLM開(kāi)發(fā)框架,能和外部數(shù)據(jù)源交互、能集成各種常用的組件等等,大大降低了LLM應(yīng)用開(kāi)發(fā)的門(mén)檻。其創(chuàng)始人Harrison Chase也和Andrew Ng聯(lián)合開(kāi)發(fā)了2門(mén)短課程,幫忙大家快速掌握LangChain的使用。
目前大模型的迭代升級(jí)特別快,作為一個(gè)框架,LangChain也得保持特別快的迭代速度。其開(kāi)發(fā)特別拼,每天都會(huì)提交大量的commit,基本隔幾天就會(huì)發(fā)布一個(gè)新版本,其Contributor也達(dá)到了1200多人,特別活躍。
個(gè)人認(rèn)為,除了和業(yè)務(wù)結(jié)合落地LLM應(yīng)用外,還有2個(gè)大的方向可以進(jìn)一步去探索:
1)通過(guò)低代碼的形式進(jìn)一步降低LLM應(yīng)用的開(kāi)發(fā)門(mén)檻。類似langflow這樣的可視化編排工具發(fā)展也很快
2)打造更加強(qiáng)大的Agent。Agent之于大模型,個(gè)人覺(jué)得類似SQL之于DB,能大幅度提升LLM的應(yīng)用場(chǎng)景
審核編輯:黃飛
?
評(píng)論
查看更多