我们在本地使用大模型的时候,尤其是构建RAG应用的时候,一般会有2个成熟的框架可以使用
- LangChain:用开发LLM的通用框架。
- LlamaIndex:专门用于构建RAG系统的框架。
选择一个框架是对于项目的后续开发是非常重要的,因为如果后续更换框架是一个非常困难的事情,所以我们这里对这两个框架做一个简单的对比,这样对于选择会有一个初步的印象。
首先我们看看他们的Github表现和一些公开的信息:
从财务状况来看,LlamaIndex的融资规模接近LangChain,尽管他们的目标市场要小得多。这可能表明LlamaIndex的生存机会更大,因为资金比较宽裕。但是LangChain提供了更多的面向企业并且能够产生收入的产品(LangServe, LangSmith),所以可能LangChain的收入更高,这样看来LangChain会更好。。
上面是我们对企业资金方面的胡乱分析,仅供参考。下面让我们进入正题,在本文中我将使用两个框架并行完成一些基本任务。通过对比展示这些代码片段,我希望它能在你做出选择时有所帮助。
1、用本地LLM创建聊天机器人
第一个任务是制作一个聊天机器人,并且使用本地的LLM。
虽然是本地,但是我们让LLM在独立的推理服务器中运行,这样可以避免重复使用,2个框架直接使用同一服务即可。虽然LLM推理API有多种模式,但我们这里选择与OpenAI兼容的模式,这样如果切换成OpenAI的模型也不需要修改代码。
下面是LlamaIndex的方法:
from llama_index.llms import ChatMessage, OpenAILike llm = OpenAILike( api_base="http://localhost:1234/v1", timeout=600, # secs api_key="loremIpsum", is_chat_model=True, context_window=32768, ) chat_history = [ ChatMessage(role="system", content="You are a bartender."), ChatMessage(role="user", content="What do I enjoy drinking?"), ] output = llm.chat(chat_history) print(output)
这是LangChain:
from langchain.schema import HumanMessage, SystemMessage from langchain_openai import ChatOpenAI llm = ChatOpenAI( openai_api_base="http://localhost:1234/v1", request_timeout=600, # secs, I guess. openai_api_key="loremIpsum", max_tokens=32768, ) chat_history = [ SystemMessage(content="You are a bartender."), HumanMessage(content="What do I enjoy drinking?"), ] print(llm(chat_history))
可以看到代码十分类似:
LangChain区分了聊天llm (ChatOpenAI)和llm (OpenAI),而LlamaIndex在构造函数中使用is_chat_model参数来进行区分。
LlamaIndex区分官方OpenAI端点和openaillike端点,而LangChain通过openai_api_base参数决定向何处发送请求。
LlamaIndex用role参数标记聊天消息,而LangChain使用单独的类。
2个框架基本没什么差别,我们继续
2、为本地文件构建RAG系统
我们构建一个简单的RAG系统:从本地的文本文件文件夹中读取文本。
以下是使用LlamaIndex文档的代码:
from llama_index import ServiceContext, SimpleDirectoryReader, VectorStoreIndex service_context = ServiceContext.from_defaults( embed_model="local", llm=llm, # This should be the LLM initialized in the task above. ) documents = SimpleDirectoryReader( input_dir="mock_notebook/", ).load_data() index = VectorStoreIndex.from_documents( documents=documents, service_context=service_context, ) engine = index.as_query_engine( service_context=service_context, ) output = engine.query("What do I like to drink?") print(output)
使用LangChain,代码会变得很长:
from langchain_community.document_loaders import DirectoryLoader # pip install "unstructured[md]" loader = DirectoryLoader("mock_notebook/", glob="*.md") docs = loader.load() 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") def format_docs(docs): return " ".join(doc.page_content for doc in docs) from langchain_core.runnables import RunnablePassthrough rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm # This should be the LLM initialized in the task above. ) print(rag_chain.invoke("What do I like to drink?"))
这些代码片段清楚地说明了这两个框架的不同抽象级别。LlamaIndex用一个名为“query engines”的方法封装了RAG管道,而LangChain则需要更多的内部组件:包括用于检索文档的连接器、表示“基于X,请回答Y”的提示模板,以及他所谓的“chain”(如上面的LCEL所示)。
当使用LangChain构建时,必须确切地知道想要什么。比如调用from_documents的位置,这使得对于初学者来说是一个非常麻烦的事情,需要更多的学习曲线。
LlamaIndex可以无需显式选择矢量存储后端直接使用,而LangChain则需要显示指定这也需要更多的信息,因为我们不确定在选择数据库时是否做出了明智的决定。
虽然LangChain和LlamaIndex都提供类似于Hugging Face的云服务(即LangSmith Hub和LlamaHub),但是LangChain把它集成到了几乎所有的功能,我们使用pull只下载一个简短的文本模板,内容如下:
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:**
这绝对是一种过度的做法。虽然这确实鼓励在社区中分享提示,但是这有必要吗。
3、支持RAG的聊天机器人
我们将上面两个简单的功能整合起来,这样我们可以获得一个可以和本地文件对话的真正的可用的简单应用。
使用LlamaIndex,就像将as_query_engine与as_chat_engine交换一样简单:
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时,按照官方教程,让我们首先定义memory(负责管理聊天记录):
# 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开始时,我们需要从memory中加载聊天历史记录。
load_history_from_memory = RunnableLambda(memory.load_memory_variables) | itemgetter( "history" ) load_history_from_memory_and_carry_along = RunnablePassthrough.assign( chat_history=load_history_from_memory )
然后要求LLM用上下文来丰富我们的提问
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: {chat_history} Now the user asked: {question} To answer this question, you need to look up from their notes about """ ) | llm | StrOutputParser() )
但是我们不能只是将两者连接起来,因为话题可能在谈话过程中发生了变化,这使得聊天记录中的大多数语义信息无关紧要。
然后就是运行RAG。
retrieve_documents = { "docs": itemgetter("standalone_question") | retriever, "question": itemgetter("standalone_question"), }
对提问进行回答:
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: {chat_history} Now the user asked: {question} To answer this question, you need to look up from their notes about """ ) | llm | StrOutputParser() )
得到最终响应后将其附加到聊天历史记录。
final_chain = ( load_history_from_memory_and_carry_along | {"standalone_question": rephrase_the_question} | retrieve_documents | compose_the_final_answer ) # Demo. 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."
这是一个非常复杂的过程,我们通过这个过程可以了解了很多关于llm驱动的应用程是如何构建的。特别是调用了LLM几次,让它假设不同的角色:查询生成器、总结检索到的文档的人,对话的参与者。这对于学习来说是非常有帮助的,但是对于应用是不是有些复杂了。
4、Agent
RAG管道可以被认为是一个工具。而LLM可以访问多个工具,比如给它提供搜索、百科查询、天气预报等。通过这种方式聊天机器人可以回答关于它直接知识之外的问题。
工具也不一定要提供信息,还可以进行其他操作,例如下购物订单,回复电子邮件等。
LLM有了这些工具,就需要决定使用哪些工具,以及以什么顺序使用。而使用这些工具LLM角色被称为“代理”。
有多种方式可以为LLM提供代理。最具模型泛型的方法是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, metadata=ToolMetadata( name="look_up_notes", description="Gives information about the user.", ), ) from llama_index.agent import ReActAgent agent = ReActAgent.from_tools( tools=[notes_query_engine_tool], llm=llm, 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."
对于我们的后续问题“how do I brew coffee”,代理的回答与它仅仅是一个查询引擎时不同。这是因为代理可以自己决定是否查看我们本地笔记。如果他们有足够的信心来回答这个问题,代理可能会选择不使用任何工具。如果LLM发现他无法回答这个问题,则会使用RAG搜索我们本地的文件(我们的查询引擎的其职责是从索引中查找文档,所以他肯定会选择这个)。
代理是LangChain高级API:
from langchain.agents import AgentExecutor, Tool, create_react_agent tools = [ Tool( name="look_up_notes", func=rag_chain.invoke, 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? AI: You enjoy drinking coffee.", } ) print(result) # "You can use a drip coffee maker, French press, pour-over, or espresso machine."
尽管我们仍然需要手动管理聊天记录,但与创建RAG相比,创建代理要容易得多。create_react_agent和AgentExecutor整合了底层的大部分工作。
总结
LlamaIndex和LangChain是构建LLM应用程序的两个框架。LlamaIndex专注于RAG用例,LangChain得到了更广泛的应用。我们可以看到,如果是和RAG相关的用例,LlamaIndex会方便很多,可以说是首选。
但是如果你的应用需要一些非RAG的功能,可能LangChain是一个更好的选择。
https://avoid.overfit.cn/post/94eb3dd8122346d393b059e0f9142335