拥有正确的数据来支持用例对于在任何业务中成功采用大型语言模型(LLM)都是至关重要的。虽然大多数现成的LLM在完成一般任务上表现出色,但它们在处理特定的业务问题时可能会遇到困难。它们没有针对开发人员的业务问题进行数据训练,因此没有足够的场景来解决问题。
企业通常拥有大量的内部数据和文档,可以满足特定场景的需求。但是有一个问题:如何将所有这些有用的数据(场景)集成到LLM中,而不需要进行资源密集型和耗时的再培训或微调LLM?
其答案是检索增强生成(RAG),这是一种通过实时检索紧密场景信息来增强LLM的技术。
本文将介绍如何使用LlamaIndex和LangChain来实现LLM场景数据的存储和检索。将通过使用LlamaIndex解决一个特定于场景的RAG问题,然后将解决方案轻松地部署到Heroku。
在开始编码之前,首先简要介绍一下核心概念。
RAG和LlamaIndex简介
当向LLM提出一个需要场景来回答的问题时,RAG会检索场景数据帮助LLM给出更准确、更具体的回答。这就像让厨师迅速前往农贸市场去采购储藏室没有的最新鲜的食材一样,这样厨师长就可以采用所有必要的食材烹制出完美的菜肴。
RAG工作流如何提供场景的一个关键是使用矢量数据库和矢量搜索索引。以下了解一些核心概念以及实现这一切所包含的内容。
- 向量是一组编码数字,表示一段文本(例如单词、短语、句子,甚至整个文档)的含义和场景。
- 嵌入是向量中的实际数值,但大多数人倾向于交替使用 “向量”和“嵌入”这两个术语。
- 已经在文档上训练了嵌入模型,以便它可以将新输入的文本转换为向量。并非所有的文本都以相同的方式谈论相同的事情——考虑学术研究论文与营销材料的不同。因此,有不同的嵌入模型——每个模型都在特定的数据集上训练,并且考虑特定的目标。
- 使用嵌入模型,可以从文档中创建嵌入,将这些文档中的文本分解为编码数字。创建嵌入可能涉及到文档分块这样的策略,它将大文档分成更小的、可管理的部分。从那里,每个块被转换成一个嵌入。
- 当查询一个向量数据库时,其问题被转换成一个嵌入,并与存储在矢量数据库中的所有其他嵌入进行比较。
- 当建立一个向量搜索索引,可以执行非常快速和准确的向量搜索(也称为相似性搜索)。使用矢量数据库可以执行快速而准确的搜索——不仅仅是像在传统数据库中那样匹配特定字符串的存在,而且还可以匹配与使用的单词在意义上相似的文档。
在RAG场景中,使用原始提示符对矢量数据库中的所有文档执行矢量搜索。然后,将这些匹配的文档作为场景发送到LLM应用程序。LLM现在有一组详细的注释,在对原始提示进行回答时可以参考。
LlamaIndex是一个关键的框架,它简化了集成、组织和检索私有或专用数据的过程。它将帮助开发人员创建文档嵌入和矢量搜索索引。然后,将依靠LangChain将其拼凑在一起,执行相似性搜索并将结果发送到LLM以获取响应。LlamaIndex和LangChain共同为处理RAG工作流提供了一个安全可靠的解决方案。
准备好做些什么了吗?现在开始吧!
演示项目简介
使用LlamaIndex和Heroku学习RAG的最好方法是构建一个小型的示例应用程序。出于开发人员的目的,假设正在与“古登堡计划”(Project Gutenberg)合作,这是一个拥有70000多本免费电子书的图书馆。如果想要构建一个基于LLM的聊天机器人,它可以回答关于项目中免费书籍的特定问题。
这是使用RAG的完美用例,可以使用LlamaIndex获得的大量书籍文本。为了使项目简单,将使用公元401年问世的《圣奥古斯丁的忏悔录》书中的内容。
完成的项目代码库可以在这个GitHub存储库(https://github.com/alvinslee/llamaindex-gutenberg-demo)中找到。如果愿意的话,可以克隆repo并将应用程序部署到Heroku。或者,可以逐步了解如何获得代码。
构建这个演示项目将遵循以下一些步骤:
- 设置项目。
- 加载数据。
- 构建索引。
- 存储索引。
- 集成LangChain。
- 部署到Heroku。
步骤1:设置项目
为Python项目创建一个新文件夹,然后激活venv并安装需要的初始依赖项。
Shell
1 (venv) ~/project$ pip install llama-index langchain langchain-openai
接下来,将加载要索引的数据。
步骤2:加载数据
在构建用于RAG的内部数据索引时,必须将所有数据(文本)收集到一个地方。在这个例子中,该项目采用了《圣奥古斯丁的忏悔录》的文本。将使用LlamaIndex将这个场景转换为嵌入的矢量索引。
在典型的用例中,其场景数据将是适合试图解决的业务问题的大型文本语料库。
对于这个小型演示项目,将创建一个名为data的子文件夹,然后将该书作为单个文件下载到该文件夹中。
Shell
1(venv) ~/project$ mkdir data
2
3 (venv) ~/project$ curl \
4 https://www.gutenberg.org/cache/epub/3296/pg3296.txt \
5 -o data/confessions.txt
6
7
8 (venv) ~/project$ ls data
9 confessions.txt
步骤3:构建索引
在一个目录中收集了所有数据之后,就可以构建索引了。将编写一个简单的Python应用程序,它将使用LlamaIndex为数据建立索引,然后查询索引。
为此,需要一个OpenAI帐户和API密钥。这是因为LlamaIndex使用OpenAI的text-embedding-3-small作为默认嵌入模型 (更改这些默认值超出了本文的讨论范围) 。
在这个项目的根文件夹中,创建了一个名为index.py的文件。初始内容如下所示:
Python
1 # index.py
2
3 from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
4 import os
5
6 if os.environ.get('OPENAI_API_KEY') is None:
7 exit('You must provide an OPENAI_API_KEY env var.')
8
9 documents = SimpleDirectoryReader("data").load_data()
10 index = VectorStoreIndex.from_documents(documents)
11
12 query_engine = index.as_query_engine()
13 response = query_engine.query("In which city is Saint Augustine the Bishop?")
14 print(response)
运行文件并收到预期的响应:
Shell
1 (venv) ~/project$ OPENAI_API_KEY=sk-******** python index.py
2 Hippo
当然,可以再次检查数据。看看这本书的前几行,可以看到:
Shell
1 THE CONFESSIONS OF SAINT AUGUSTINE
2
3 By Saint Augustine
4
5 Bishop of Hippo
正如人们所看到的,LlamaIndex完成了它的工作。Python应用程序完全按照开发人员对向量索引数据的期望回答了问题。
步骤4:存储索引
需要注意的是,在上面的示例中,只将索引数据存储在内存中,而不是磁盘上。索引(现在是内存中的一系列向量嵌入)将在调用OpenAI模型并完成工作流后完全丢失。
为文本创建向量索引(嵌入)不是免费的,所以不想每次调用模型时都重新计算这些结果。最好有一个单独的工作流将索引持久化到磁盘。然后,可以在任何时候引用它。
一种常见的方法是将嵌入存储在PostgreSQL数据库中,并使用pgvector执行相似性搜索。为了使演示简单,只将索引数据存储为平面文件。
因此,将这个简单的步骤添加到index.py文件中:
Python
1 PERSIST_DIR='./my_vector_indexes/gutenberg/'
2 index.storage_context.persist(persist_dir=PERSIST_DIR)
现在,在运行文件之后,可以检查存储的索引。
Shell
1 (venv) ~/project$ OPENAI_API_KEY=sk-******** python index.py
2 Hippo
3
4 (venv) ~/project$ tree
5 .
6├── data
7│ └── confessions.txt
8├── index.py
9└── my_vector_indexes
10 └── gutenberg
11 ├── default__vector_store.json
12 ├── docstore.json
13 ├── graph_store.json
14 ├── image__vector_store.json
15 └── index_store.json
16
17 3 directories, 7 files
步骤5:整合LangChain
已经了解矢量索引存储的基础知识,以及构建一个矢量索引存储是多么容易。但是,为了真正构建一个将所有内容链接在一起的端到端应用程序,可以使用LangChain。这样,就可以将解决方案部署为API。可以重写index.py代码,使其更适合生产环境。
以下将展示代码,然后解释接下来要做的事情。它可能看起来像很多代码,但只添加了一些新步骤。
Python
1 # index.py
2
3 import os
4 from langchain_openai import ChatOpenAI
5 from langchain.chains import ConversationalRetrievalChain
6 from langchain.memory import ConversationBufferWindowMemory
7 from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
8 from langchain_community.retrievers import LlamaIndexRetriever
9 from fastapi import FastAPI
10 from pydantic import BaseModel
11
12 if os.environ.get('OPENAI_API_KEY') is None:
13 exit('You must provide an OPENAI_API_KEY env var.')
14
15 documents = SimpleDirectoryReader("data").load_data()
16 index = VectorStoreIndex.from_documents(documents)
17
18 # For this demo, we will not persist the index.
19
20
21 retriever = LlamaIndexRetriever(index=index.as_query_engine())
22
23 llm = ChatOpenAI(model_name="gpt-3.5-turbo", max_tokens=2048)
24
25 memory = ConversationBufferWindowMemory(
26 memory_key='chat_history',
27 return_messages=True,
28 k=3
29 )
30
31 conversation = ConversationalRetrievalChain.from_llm(
32 llm=llm,
33 retriever=retriever,
34 memory=memory,
35 max_tokens_limit=1536
36 )
37
38 class Prompt(BaseModel):
39 question: str
40
41 app = FastAPI()
42
43 @app.post("/prompt")
44 async def query_chatbot(prompt: Prompt):
45 response = conversation.invoke({'question': prompt.question})
46 return response['answer']
47
48 if __name__=='__main__':
49 import uvicorn
50 uvicorn.run(app, host="localhost", port=8000)
首先,要注意的是,现在直接使用了LangChain和OpenAI。将LLM与一些内存一起设置,以便在后续查询中“记住”对话。现在有一个真正的聊天机器人,可以与之互动。
从这里,使用FastAPI创建一个API服务器,该服务器监听/prompt端点上的POST请求。对该端点的请求预计具有带有问题的请求体,然后将其(连同来自向量索引的场景)传递给LLM。
使用uvicorn在端口8000上启动服务器。
在启动服务器之前,添加这些新的Python依赖项:
Shell
1(venv) ~/project$ pip install fastapi pydantic uvicorn
现在是测试的时候了。首先启动服务器。
Shell
1 (venv) ~/project$ OPENAI_API_KEY=sk-******** python index.py
2INFO: Started server process [1101807]
3 INFO: Waiting for application startup.
4 INFO: Application startup complete.
5 INFO: Uvicorn running on http://localhost:8000 (Press CTRL+C to quit)
在另一个终端中,向端点发送一个curl请求。
Shell
1 $ curl -X POST \
2 --header "Content-type:application/json" \
3 --data '{"question":"Who is Ambrose?"}' \
4 http://localhost:8000/prompt
5
6
7 "Ambrose is a person mentioned in the text provided. He is described as a respected
8 and celibate man who was esteemed by the author. Ambrose is depicted as a figure of
9 great honor and excellence, particularly known for his dedication to reading and
10 studying."
11
获得成功!向量索引似乎已经启动并运行,这个聊天机器人功能齐全,是部署的时候了。
步骤6:部署到Heroku
在完成了主要的工作之后,只需要采取几个简单的步骤将应用程序部署到Heroku。
(1)用Python依赖项创建requirements.txt文件
Heroku需要知道在构建项目时要安装哪些Python依赖项。它在一个名为requirements.txt的文件中查找这个列表。可以用下面的命令轻松地生成:
Shell
1(venv) ~/project$ pip freeze > requirements.txt
(2)创建Procfile
还需要告诉Heroku如何启动Python应用程序。在一个名为Procfile的文件中执行这一操作。
Shell
1 (venv) ~/project$ echo \
2 'web: uvicorn index:app --host=0.0.0.0 --port=${PORT}' > Procfile
(3)创建runtime.txt文件
最后,runtime.txt将告诉Heroku希望使用哪种Python运行时版本。
Shell
1 (venv) ~/project$ echo 'python-3.11.8' > runtime.txt
这些都是需要的文件。这时项目文件夹结构应该看起来像(已经删除了持久化的矢量索引):
Shell
1 ~/project$ tree
2 .
3├── data
4│ └── confessions.txt
5├── index.py
6├── Procfile
7├── requirements.txt
8└── runtime.txt
9
10 1 directory, 5 files
如果开发人员是从头开始工作,并且没有为这个演示项目克隆GitHub仓库,那么将这些文件提交到自己的Git存储库。
(4)创建Heroku应用程序
下载并安装Heroku CLI后,执行如下命令。开发人员可以为其应用程序选择任何名字,需要提供唯一的OpenAI API密钥。
Shell
1 ~/project$ heroku login
2
3 ~/project$ heroku apps:create my-llamaindex-app
4
5 ~/project$ heroku git:remote -a my-llamaindex-app
6
7 ~/project$ heroku config:add OPENAI_API_KEY=replaceme -a my-llamaindex-app
8
9 ~/project$ git push heroku main
10 …
11 remote: -----> Building on the Heroku-22 stack
12 remote: -----> Determining which buildpack to use for this app
13 remote: -----> Python app detected
14 remote: -----> Using Python version specified in runtime.txt
15 …
16 remote: -----> Launching...
17 remote: Released v4
18 remote: https://my-llamaindex-app-6b48faa3ee6a.herokuapp.com/ deployed to Heroku
19
部署应用程序后,通过向API服务器发送curl请求进行测试:
Shell
1 $ curl -X POST \
2 --header "Content-type:application/json" \
3 --data '{"question":"Who is Ambrose?"}' \
4 https://my-llamaindex-app-6b48faa3ee6a.herokuapp.com/prompt
5
6 "Ambrose is a significant figure in the text provided. He is being described as a
7 respected and happy man, known for his celibacy and his dedication to reading and
8 studying. He is referred to as a holy oracle and a person of great influence and
9 wisdom."
10
需要记住的是,上面的curl调用在部署中使用了唯一的Heroku应用URL。
现在已经在Heroku上运行了!
结论
现在已经清楚地了解了LlamaIndex的强大功能,以及它在构建RAG应用程序与LLM交互时所扮演的重要角色。当可以很容易地添加特定的数据源作为LLM的场景,而不需采用成本昂贵的模型再训练时,这是一个巨大的胜利。而对于希望进一步推进LLM工作流程的公司和开发人员来说,这也是一个胜利。
将LlamaIndex与其他LangChain工具集结合起来也是无缝且直接的,构建聊天机器人只需要几行额外的代码。最后,能够快速轻松地将解决方案部署到Heroku,使应用程序可以立即访问,而不会有任何麻烦。像这样的简单部署使开发人员能够专注于构建基于LLM的解决方案这一更复杂、更重要的任务。
原文标题:How To Implement RAG: A Simple Walkthrough,作者:Alvin Lee
链接:https://dzone.com/articles/how-to-implement-rag-a-simple-walkthrough。