吾名爱妃,性好静亦好动。好编程,常沉浸于代码之世界,思维纵横,力求逻辑之严密,算法之精妙。亦爱篮球,驰骋球场,尽享挥洒汗水之乐。且喜跑步,尤钟马拉松,长途奔袭,考验耐力与毅力,每有所进,心甚喜之。
吾以为,编程似布阵,算法如谋略,需精心筹谋,方可成就佳作。篮球乃团队之艺,协作共进,方显力量。跑步与马拉松,乃磨炼身心之途,愈挫愈勇,方能达至远方。愿交志同道合之友,共探此诸般妙趣。诸君,此文尚佳,望点赞收藏,谢之!
我们在本地使用大模型的时候,尤其是构建RAG应用的时候,一般会有2个成熟的框架可以使用:
-
LangChain:用开发LLM的通用框架。
-
LlamaIndex:专门用于构建RAG系统的框架。
本文中我将使用两个框架并行完成一些基本任务。通过对比展示这些代码片段,我希望它能在你做出选择时有所帮助。
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 "\n\n".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?"))
可以看到LangChain的实现代码变得很长!
这些代码片段清楚地说明了这两个框架的不同抽象级别。LlamaIndex用一个名为“query engines”的方法封装了RAG管道,而LangChain则需要更多的内部组件:包括用于检索文档的连接器、表示“基于X,请回答Y”的提示模板,以及他所谓的“chain”(如上面的LCEL所示)。
当使用LangChain构建时,必须确切地知道想要什么。比如调用from_documents的位置,这使得对于初学者来说是一个非常麻烦的事情,需要更多的学习曲线。
LlamaIndex可以无需显式选择矢量存储后端直接使用,而LangChain则需要显示指定这也需要更多的信息,因为我们不确定在选择数据库时是否做出了明智的决定。
3、支持RAG的聊天机器人
我们将上面两个简单的功能整合起来,这样我们可以获得一个可以和本地文件对话的真正的可用的简单应用。
LlamaIndex的实现:
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."
就像将as_query_engine与as_chat_engine交换一样简单
LangChain的实现:
# 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"
)
按照官方教程,让我们首先定义memory(负责管理聊天记录)
在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?\nAI: 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整合了底层的大部分工作。
5、总结
LlamaIndex和LangChain是构建LLM应用程序的两个框架。LlamaIndex专注于RAG用例,LangChain得到了更广泛的应用。我们可以看到,如果是和RAG相关的用例,LlamaIndex会方便很多,可以说是首选。
但是如果你的应用需要一些非RAG的功能,可能LangChain是一个更好的选择。