我們?cè)诒镜厥褂么竽P偷臅r(shí)候,,尤其是構(gòu)建RAG應(yīng)用的時(shí)候,一般會(huì)有2個(gè)成熟的框架可以使用:
1)LangChain:用開(kāi)發(fā)LLM的通用框架,。
2)LlamaIndex:專門(mén)用于構(gòu)建RAG系統(tǒng)的框架,。
選擇一個(gè)框架是對(duì)于項(xiàng)目的后續(xù)開(kāi)發(fā)是非常重要的,因?yàn)槿绻罄m(xù)更換框架是一個(gè)非常困難的事情,,所以我們這里對(duì)這兩個(gè)框架做一個(gè)簡(jiǎn)單的對(duì)比,,這樣對(duì)于選擇會(huì)有一個(gè)初步的印象,。
首先我們看看他們的Github表現(xiàn)和一些公開(kāi)的信息:
從財(cái)務(wù)狀況來(lái)看,LlamaIndex的融資規(guī)模接近LangChain,,盡管他們的目標(biāo)市場(chǎng)要小得多,。這可能表明LlamaIndex的生存機(jī)會(huì)更大,因?yàn)橘Y金比較寬裕,。但是LangChain提供了更多的面向企業(yè)并且能夠產(chǎn)生收入的產(chǎn)品(LangServe, LangSmith),,所以可能LangChain的收入更高,,這樣看來(lái)LangChain會(huì)更好,。
上面是我們對(duì)企業(yè)資金方面的胡亂分析,僅供參考,。下面讓我們進(jìn)入正題,,在本文中我將使用兩個(gè)框架并行完成一些基本任務(wù)。通過(guò)對(duì)比展示這些代碼片段,,我希望它能在你做出選擇時(shí)有所幫助,。
1、用本地LLM創(chuàng)建聊天機(jī)器人
第一個(gè)任務(wù)是制作一個(gè)聊天機(jī)器人,,并且使用本地的LLM,。
雖然是本地,但是我們讓LLM在獨(dú)立的推理服務(wù)器中運(yùn)行,,這樣可以避免重復(fù)使用,,2個(gè)框架直接使用同一服務(wù)即可。雖然LLM推理API有多種模式,,但我們這里選擇與OpenAI兼容的模式,,這樣如果切換成OpenAI的模型也不需要修改代碼。
下面是LlamaIndex的方法:
from llama_index.llms import ChatMessage, OpenAILike
api_base="http://localhost:1234/v1", ChatMessage(role="system", content="You are a bartender."), ChatMessage(role="user", content="What do I enjoy drinking?"), output = llm.chat(chat_history)
這是LangChain:
from langchain.schema import HumanMessage, SystemMessage from langchain_openai import ChatOpenAI
openai_api_base="http://localhost:1234/v1", request_timeout=600, # secs, I guess. openai_api_key="loremIpsum", SystemMessage(content="You are a bartender."), HumanMessage(content="What do I enjoy drinking?"),
可以看到代碼十分類似,。
LangChain區(qū)分了聊天llm (ChatOpenAI)和llm (OpenAI),,而LlamaIndex在構(gòu)造函數(shù)中使用is_chat_model參數(shù)來(lái)進(jìn)行區(qū)分。
LlamaIndex區(qū)分官方OpenAI端點(diǎn)和openaillike端點(diǎn),,而LangChain通過(guò)openai_api_base參數(shù)決定向何處發(fā)送請(qǐng)求,。
LlamaIndex用role參數(shù)標(biāo)記聊天消息,而LangChain使用單獨(dú)的類,。
2個(gè)框架基本沒(méi)什么差別,,我們繼續(xù)。
2,、為本地文件構(gòu)建RAG系統(tǒng)
我們構(gòu)建一個(gè)簡(jiǎn)單的RAG系統(tǒng):從本地的文本文件文件夾中讀取文本,。
以下是使用LlamaIndex文檔的代碼:
from llama_index import ServiceContext, SimpleDirectoryReader, VectorStoreIndex
service_context = ServiceContext.from_defaults( llm=llm, # This should be the LLM initialized in the task above. documents = SimpleDirectoryReader( input_dir="mock_notebook/", index = VectorStoreIndex.from_documents( service_context=service_context, engine = index.as_query_engine( service_context=service_context, output = engine.query("What do I like to drink?")
使用LangChain,代碼會(huì)變得很長(zhǎng):
from langchain_community.document_loaders import DirectoryLoader
# pip install "unstructured[md]" loader = DirectoryLoader("mock_notebook/", glob="*.md")
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200) splits = text_splitter.split_documents(docs)
from langchain_community.embeddings.fastembed import FastEmbedEmbeddings from langchain_community.vectorstores import Chroma
vectorstore = Chroma.from_documents(documents=splits, embedding=FastEmbedEmbeddings()) retriever = vectorstore.as_retriever()
from langchain import hub
# pip install langchainhub prompt = hub.pull("rlm/rag-prompt")
return "\n\n".join(doc.page_content for doc in docs)
from langchain_core.runnables import RunnablePassthrough
{"context": retriever | format_docs, "question": RunnablePassthrough()} | llm # This should be the LLM initialized in the task above. print(rag_chain.invoke("What do I like to drink?"))
這些代碼片段清楚地說(shuō)明了這兩個(gè)框架的不同抽象級(jí)別,。LlamaIndex用一個(gè)名為“query engines”的方法封裝了RAG管道,,而LangChain則需要更多的內(nèi)部組件:包括用于檢索文檔的連接器、表示“基于X,請(qǐng)回答Y”的提示模板,,以及他所謂的“chain”(如上面的LCEL所示),。
當(dāng)使用LangChain構(gòu)建時(shí),必須確切地知道想要什么,。比如調(diào)用from_documents的位置,,這使得對(duì)于初學(xué)者來(lái)說(shuō)是一個(gè)非常麻煩的事情,需要更多的學(xué)習(xí)曲線,。
LlamaIndex可以無(wú)需顯式選擇矢量存儲(chǔ)后端直接使用,,而LangChain則需要顯示指定這也需要更多的信息,因?yàn)槲覀儾淮_定在選擇數(shù)據(jù)庫(kù)時(shí)是否做出了明智的決定,。
雖然LangChain和LlamaIndex都提供類似于Hugging Face的云服務(wù)(即LangSmith Hub和LlamaHub),,但是LangChain把它集成到了幾乎所有的功能,我們使用pull只下載一個(gè)簡(jiǎn)短的文本模板,,內(nèi)容如下:
You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don’t know the answer, just say that you don’t know. Use three sentences maximum and keep the answer concise.Question: {question}Context: {context}Answer:**
這絕對(duì)是一種過(guò)度的做法,。雖然這確實(shí)鼓勵(lì)在社區(qū)中分享提示,但是這有必要嗎,。
3,、支持RAG的聊天機(jī)器人
我們將上面兩個(gè)簡(jiǎn)單的功能整合起來(lái),這樣我們可以獲得一個(gè)可以和本地文件對(duì)話的真正的可用的簡(jiǎn)單應(yīng)用,。
使用LlamaIndex,,就像將as_query_engine與as_chat_engine交換一樣簡(jiǎn)單:
engine = index.as_chat_engine() output = engine.chat("What do I like to drink?") print(output) # "You enjoy drinking coffee." output = engine.chat("How do I brew it?") print(output) # "You brew coffee with a Aeropress."
使用LangChain時(shí),按照官方教程,,讓我們首先定義memory(負(fù)責(zé)管理聊天記錄):
# Everything above this line is the same as that of the last task. from langchain_core.runnables import RunnablePassthrough, RunnableLambda from langchain_core.messages import get_buffer_string from langchain_core.output_parsers import StrOutputParser from operator import itemgetter from langchain.memory import ConversationBufferMemory from langchain.prompts.prompt import PromptTemplate from langchain.schema import format_document from langchain_core.prompts import ChatPromptTemplate
memory = ConversationBufferMemory( return_messages=True, output_key="answer", input_key="question"
在LLM開(kāi)始時(shí),,我們需要從memory中加載聊天歷史記錄。
load_history_from_memory = RunnableLambda(memory.load_memory_variables) | itemgetter( load_history_from_memory_and_carry_along = RunnablePassthrough.assign( chat_history=load_history_from_memory
然后要求LLM用上下文來(lái)豐富我們的提問(wèn):
rephrase_the_question = ( "question": itemgetter("question"), "chat_history": lambda x: get_buffer_string(x["chat_history"]), | PromptTemplate.from_template( """You're a personal assistant to the user. Here's your conversation with the user so far: Now the user asked: {question} To answer this question, you need to look up from their notes about """
但是我們不能只是將兩者連接起來(lái),,因?yàn)樵掝}可能在談話過(guò)程中發(fā)生了變化,,這使得聊天記錄中的大多數(shù)語(yǔ)義信息無(wú)關(guān)緊要。
然后就是運(yùn)行RAG,。
"docs": itemgetter("standalone_question") | retriever, "question": itemgetter("standalone_question"),
對(duì)提問(wèn)進(jìn)行回答:
rephrase_the_question = ( "question": itemgetter("question"), "chat_history": lambda x: get_buffer_string(x["chat_history"]), | PromptTemplate.from_template( """You're a personal assistant to the user. Here's your conversation with the user so far: Now the user asked: {question} To answer this question, you need to look up from their notes about """
得到最終響應(yīng)后將其附加到聊天歷史記錄,。
load_history_from_memory_and_carry_along | {"standalone_question": rephrase_the_question} | compose_the_final_answer inputs = {"question": "What do I like to drink?"} output = final_chain.invoke(inputs) memory.save_context(inputs, {"answer": output.content}) print(output) # "You enjoy drinking coffee." inputs = {"question": "How do I brew it?"} output = final_chain.invoke(inputs) memory.save_context(inputs, {"answer": output.content}) print(output) # "You brew coffee with a Aeropress."
這是一個(gè)非常復(fù)雜的過(guò)程,我們通過(guò)這個(gè)過(guò)程可以了解了很多關(guān)于llm驅(qū)動(dòng)的應(yīng)用程是如何構(gòu)建的,。特別是調(diào)用了LLM幾次,,讓它假設(shè)不同的角色:查詢生成器、總結(jié)檢索到的文檔的人,,對(duì)話的參與者,。這對(duì)于學(xué)習(xí)來(lái)說(shuō)是非常有幫助的,但是對(duì)于應(yīng)用是不是有些復(fù)雜了,。
4,、Agent
RAG管道可以被認(rèn)為是一個(gè)工具,。而LLM可以訪問(wèn)多個(gè)工具,比如給它提供搜索,、百科查詢,、天氣預(yù)報(bào)等。通過(guò)這種方式聊天機(jī)器人可以回答關(guān)于它直接知識(shí)之外的問(wèn)題,。
工具也不一定要提供信息,,還可以進(jìn)行其他操作,例如下購(gòu)物訂單,,回復(fù)電子郵件等,。
LLM有了這些工具,就需要決定使用哪些工具,,以及以什么順序使用,。而使用這些工具LLM角色被稱為“代理”,。
有多種方式可以為L(zhǎng)LM提供代理,。最具模型泛型的方法是ReAct范式。
在LlamaIndex中使用方法如下:
from llama_index.tools import ToolMetadata from llama_index.tools.query_engine import QueryEngineTool
notes_query_engine_tool = QueryEngineTool( query_engine=notes_query_engine, description="Gives information about the user.", from llama_index.agent import ReActAgent
agent = ReActAgent.from_tools( tools=[notes_query_engine_tool], service_context=service_context, output = agent.chat("What do I like to drink?") print(output) # "You enjoy drinking coffee." output = agent.chat("How do I brew it?") print(output) # "You can use a drip coffee maker, French press, pour-over, or espresso machine."
對(duì)于我們的后續(xù)問(wèn)題“how do I brew coffee”,,代理的回答與它僅僅是一個(gè)查詢引擎時(shí)不同,。這是因?yàn)榇砜梢宰约簺Q定是否查看我們本地筆記。如果他們有足夠的信心來(lái)回答這個(gè)問(wèn)題,,代理可能會(huì)選擇不使用任何工具,。如果LLM發(fā)現(xiàn)他無(wú)法回答這個(gè)問(wèn)題,則會(huì)使用RAG搜索我們本地的文件(我們的查詢引擎的其職責(zé)是從索引中查找文檔,,所以他肯定會(huì)選擇這個(gè)),。
代理是LangChain高級(jí)API:
from langchain.agents import AgentExecutor, Tool, create_react_agent
description="Gives information about the user.", react_prompt = hub.pull("hwchase17/react-chat") agent = create_react_agent(llm, tools, react_prompt) agent_executor = AgentExecutor.from_agent_and_tools(agent=agent, tools=tools)
result = agent_executor.invoke( {"input": "What do I like to drink?", "chat_history": ""} print(result) # "You enjoy drinking coffee." result = agent_executor.invoke( "input": "How do I brew it?", "chat_history": "Human: What do I like to drink?\nAI: You enjoy drinking coffee.", print(result) # "You can use a drip coffee maker, French press, pour-over, or espresso machine."
盡管我們?nèi)匀恍枰謩?dòng)管理聊天記錄,但與創(chuàng)建RAG相比,,創(chuàng)建代理要容易得多,。create_react_agent和AgentExecutor整合了底層的大部分工作。
5,、總結(jié)
LlamaIndex和LangChain是構(gòu)建LLM應(yīng)用程序的兩個(gè)框架,。LlamaIndex專注于RAG用例,LangChain得到了更廣泛的應(yīng)用,。我們可以看到,,如果是和RAG相關(guān)的用例,LlamaIndex會(huì)方便很多,,可以說(shuō)是首選,。
但是如果你的應(yīng)用需要一些非RAG的功能,可能LangChain是一個(gè)更好的選擇,。
轉(zhuǎn)自:通過(guò)4個(gè)任務(wù)比較LangChain和LlamaIndex
|