在Why RAG is big中,我表示支持检索增强生成(RAG)作为私有、离线、去中心化 LLM 应用程序的关键技术。 当你建造一些东西供自己使用时,你就是在孤军奋战。 你可以从头开始构建,但在现有框架上构建会更有效。
NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - Three.js虚拟轴心开发包
AFAIK,存在两种选择,针对不同的范围:
选择一个框架是一项巨大的投资。 你想要一个拥有强大维护者和充满活力的社区的产品。 幸运的是,这两种选择在去年都已合并,因此规模是相当可量化的。 以下是这些数字的比较:
从财务数据来看,LlamaIndex 表现强劲,融资金额接近LangChain,但其目标市场要小得多(以 GitHub 星数作为社区兴趣的近似值)。 这可能表明 LlamaIndex 有更好的生存机会。 话虽这么说,LangChain提供了更多面向企业的、可以产生收入的产品(LangServe、LangSmith……),所以这个论点可能会颠倒过来。 从货币角度来看,这是一个艰难的决定。
我的财务 101只能带我到此为止。 让我们谈谈我真正擅长的领域并用 Python 进行讨论。 在本文中,我将使用这两个框架并行完成一些基本任务。 通过并排呈现代码片段,我希望它可以帮助你做出更明智的决定,决定在你自己的 RAG 聊天机器人中使用哪些代码片段。
对于要实现的第一个任务,我选择制作一个仅限本地的聊天机器人。 这是因为我不想在学习使用这些框架时为模拟聊天消息支付云服务费用。
我选择让 LLM 在独立的推理服务器中运行,而不是让框架在每次运行脚本时将数 GB 模型加载到内存中。 这样可以节省时间并避免磁盘磨损。
虽然 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))
对于这两种情况,API 密钥可以是任意的,但必须存在。 我猜想这是在两个框架中运行的 OpenAI SDK 的要求。
到目前为止,这两个框架的情况看起来并没有太大不同。 让我们继续吧。
有了LLM的联系,我们就可以开始做生意了。 现在让我们构建一个简单的 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?"))
这些片段清楚地说明了这两个框架的不同抽象级别。 LlamaIndex 使用一个名为“查询引擎”的便捷包包装 RAG 管道,而 LangChain 则向您展示内部组件。 它们包括检索文档的串联器、“基于 X 请回答 Y”的提示模板以及链本身(如上面的 LCEL 所示)。
这种抽象的缺乏对学习者有影响:当使用 LangChain 进行构建时,你必须在第一次尝试时准确地知道你想要什么。 例如,比较 from_documents 的调用位置。 LlamaIndex 允许您在不显式选择存储后端的情况下使用向量存储索引,而 LangChain 似乎建议您立即选择一个实现。 (每个人在使用 LangChain 从文档创建向量索引时似乎都明确选择了后端。)在遇到可扩展性问题之前,我不确定在选择数据库时是否做出了明智的决定。
更有趣的是,虽然LangChain和LlamaIndex都提供类似Hugging Face Hub的云服务(即LangSmith Hub和LlamaHub),但拨到11的是LangChain。注意LangChain的hub.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:
虽然这确实鼓励与社区分享雄辩的提示,但我觉得这是一种矫枉过正。 存储约 1kB 的文本并不能真正证明拉取所涉及的网络调用是合理的。 我希望下载的工件被缓存。
到目前为止,我们一直在构建不太智能的东西。 在第一个任务中,我们构建了一个可以保持对话但不太了解你的东西; 第二个,我们构建了一些了解您但不保留聊天记录的东西。 让我们将这两者结合起来。
使用 LlamaIndex,就像将 as_query_engine 与 as_chat_engine 交换一样简单:
# Everything from above, till and including the creation of the index.
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,我们需要把很多事情说清楚。 按照官方教程,我们先来定义一下内存:
# 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"
)
计划如下:
1、在LLM开始时,我们从内存中加载聊天记录。
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
)
2、我们要求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()
)
我们不能只是将两者连接起来,因为话题可能在对话过程中发生了变化,使得聊天日志中的大多数语义信息变得无关紧要。
3、我们运行 RAG 管道。 请注意,我们如何通过暗示“我们作为用户将自己查找注释”来欺骗LLM,但实际上我们现在要求LLM承担繁重的工作。 我心情不好。
retrieve_documents = {
"docs": itemgetter("standalone_question") | retriever,
"question": itemgetter("standalone_question"),
}
4、我们问LLM:“以检索到的文档作为参考(以及可选的迄今为止的对话),您对用户最新问题的回应是什么?”
def _combine_documents(docs):
prompt = PromptTemplate.from_template(template="{page_content}")
doc_strings = [format_document(doc, prompt) for doc in docs]
return "\n\n".join(doc_strings)
compose_the_final_answer = (
{
"context": lambda x: _combine_documents(x["docs"]),
"question": itemgetter("question"),
}
| ChatPromptTemplate.from_template(
"""You're a personal assistant.
With the context below:
{context}
To the question "{question}", you answer:"""
)
| llm
)
5、我们将最终回复附加到聊天记录中。
# Putting all 4 stages together...
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,让它呈现不同的角色:查询生成器、总结检索到的文档的人,最后是我们对话的参与者。 我也希望您现在已经充分熟悉 LCEL。
如果将与你交谈的 LLM 角色视为一个人,那么 RAG 管道可以被视为该人使用的工具。 一个人可以使用多种工具,LLM也可以。 你可以给它提供搜索谷歌、查找维基百科、检查天气预报等的工具。通过这种方式,你的聊天机器人可以回答有关其直接知识之外的问题。
它不一定是信息工具。 通过为我们的LLM提供搜索网络、下购物订单、回复电子邮件等工具,您可以使其能够影响现实并改变世界。
工具很多,需要决定使用哪些工具以及使用顺序。 这种能力被称为Agent或智能体。 因此,具有代理权的LLM的角色被称为“代理”。
有多种方法可以为 LLM 申请提供代理权。 最模型通用(因此对自托管友好)的方式可能是 ReAct 范例,我在上一篇文章中对此进行了更多介绍。
在 LlamaIndex 中做到这一点,代码如下:
# Everything above this line is the same as in the above two tasks,
# till and including where `notes_query_engine` is defined.
# Let's convert the query engine into a tool.
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."
请注意,对于我们的后续问题“如何煮咖啡”,代理的回答与仅作为查询引擎时的回答不同。 这是因为代理可以自行决定是否从我们的笔记中查找。 如果他们有足够的信心回答问题,代理可能会选择根本不使用任何工具。 我们的“我如何……”的问题可以有两种解释:要么是关于通用选项,要么是关于事实回忆。 显然,代理选择以前一种方式理解它,而我们的查询引擎(负责从索引中查找文档)必须选择后者。
有趣的是,代理是 LangChain 决定提供高级抽象的一个用例:
# Everything above is the same as in the 2nd task, till and including where we defined `rag_chain`.
# Let's convert the chain into a tool.
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 涵盖了底层的大部分接线工作。
LlamaIndex 和 LangChain 是构建 LLM 应用程序的两个框架。 虽然 LlamaIndex 专注于 RAG 用例,但 LangChain 似乎被更广泛地采用。 但它们在实践中有何不同? 在这篇文章中,我比较了这两个框架完成四个常见任务的情况:
我希望它们能帮助你为 LLM 申请做出明智的选择。 另外,祝你在构建自己的聊天机器人的过程中一切顺利!
原文链接:LangChain vs. LlamaIndex - BimAnt