通過(guò)檢索增強(qiáng)生成(RAG) 增強(qiáng)LLM的實(shí)戰(zhàn)演練
譯文想了解更多AIGC的內(nèi)容,請(qǐng)?jiān)L問(wèn):
擁有正確的數(shù)據(jù)來(lái)支持用例對(duì)于在任何業(yè)務(wù)中成功采用大型語(yǔ)言模型(LLM)都是至關(guān)重要的。雖然大多數(shù)現(xiàn)成的LLM在完成一般任務(wù)上表現(xiàn)出色,但它們?cè)谔幚硖囟ǖ臉I(yè)務(wù)問(wèn)題時(shí)可能會(huì)遇到困難。它們沒(méi)有針對(duì)開(kāi)發(fā)人員的業(yè)務(wù)問(wèn)題進(jìn)行數(shù)據(jù)訓(xùn)練,因此沒(méi)有足夠的場(chǎng)景來(lái)解決問(wèn)題。
企業(yè)通常擁有大量的內(nèi)部數(shù)據(jù)和文檔,可以滿足特定場(chǎng)景的需求。但是有一個(gè)問(wèn)題:如何將所有這些有用的數(shù)據(jù)(場(chǎng)景)集成到LLM中,而不需要進(jìn)行資源密集型和耗時(shí)的再培訓(xùn)或微調(diào)LLM?
其答案是檢索增強(qiáng)生成(RAG),這是一種通過(guò)實(shí)時(shí)檢索緊密場(chǎng)景信息來(lái)增強(qiáng)LLM的技術(shù)。
本文將介紹如何使用LlamaIndex和LangChain來(lái)實(shí)現(xiàn)LLM場(chǎng)景數(shù)據(jù)的存儲(chǔ)和檢索。將通過(guò)使用LlamaIndex解決一個(gè)特定于場(chǎng)景的RAG問(wèn)題,然后將解決方案輕松地部署到Heroku。
在開(kāi)始編碼之前,首先簡(jiǎn)要介紹一下核心概念。
RAG和LlamaIndex簡(jiǎn)介
當(dāng)向LLM提出一個(gè)需要場(chǎng)景來(lái)回答的問(wèn)題時(shí),RAG會(huì)檢索場(chǎng)景數(shù)據(jù)幫助LLM給出更準(zhǔn)確、更具體的回答。這就像讓廚師迅速前往農(nóng)貿(mào)市場(chǎng)去采購(gòu)儲(chǔ)藏室沒(méi)有的最新鮮的食材一樣,這樣廚師長(zhǎng)就可以采用所有必要的食材烹制出完美的菜肴。
RAG工作流如何提供場(chǎng)景的一個(gè)關(guān)鍵是使用矢量數(shù)據(jù)庫(kù)和矢量搜索索引。以下了解一些核心概念以及實(shí)現(xiàn)這一切所包含的內(nèi)容。
- 向量是一組編碼數(shù)字,表示一段文本(例如單詞、短語(yǔ)、句子,甚至整個(gè)文檔)的含義和場(chǎng)景。
- 嵌入是向量中的實(shí)際數(shù)值,但大多數(shù)人傾向于交替使用 “向量”和“嵌入”這兩個(gè)術(shù)語(yǔ)。
- 已經(jīng)在文檔上訓(xùn)練了嵌入模型,以便它可以將新輸入的文本轉(zhuǎn)換為向量。并非所有的文本都以相同的方式談?wù)撓嗤氖虑椤紤]學(xué)術(shù)研究論文與營(yíng)銷材料的不同。因此,有不同的嵌入模型——每個(gè)模型都在特定的數(shù)據(jù)集上訓(xùn)練,并且考慮特定的目標(biāo)。
- 使用嵌入模型,可以從文檔中創(chuàng)建嵌入,將這些文檔中的文本分解為編碼數(shù)字。創(chuàng)建嵌入可能涉及到文檔分塊這樣的策略,它將大文檔分成更小的、可管理的部分。從那里,每個(gè)塊被轉(zhuǎn)換成一個(gè)嵌入。
- 當(dāng)查詢一個(gè)向量數(shù)據(jù)庫(kù)時(shí),其問(wèn)題被轉(zhuǎn)換成一個(gè)嵌入,并與存儲(chǔ)在矢量數(shù)據(jù)庫(kù)中的所有其他嵌入進(jìn)行比較。
- 當(dāng)建立一個(gè)向量搜索索引,可以執(zhí)行非??焖俸蜏?zhǔn)確的向量搜索(也稱為相似性搜索)。使用矢量數(shù)據(jù)庫(kù)可以執(zhí)行快速而準(zhǔn)確的搜索——不僅僅是像在傳統(tǒng)數(shù)據(jù)庫(kù)中那樣匹配特定字符串的存在,而且還可以匹配與使用的單詞在意義上相似的文檔。
在RAG場(chǎng)景中,使用原始提示符對(duì)矢量數(shù)據(jù)庫(kù)中的所有文檔執(zhí)行矢量搜索。然后,將這些匹配的文檔作為場(chǎng)景發(fā)送到LLM應(yīng)用程序。LLM現(xiàn)在有一組詳細(xì)的注釋,在對(duì)原始提示進(jìn)行回答時(shí)可以參考。
LlamaIndex是一個(gè)關(guān)鍵的框架,它簡(jiǎn)化了集成、組織和檢索私有或?qū)S脭?shù)據(jù)的過(guò)程。它將幫助開(kāi)發(fā)人員創(chuàng)建文檔嵌入和矢量搜索索引。然后,將依靠LangChain將其拼湊在一起,執(zhí)行相似性搜索并將結(jié)果發(fā)送到LLM以獲取響應(yīng)。LlamaIndex和LangChain共同為處理RAG工作流提供了一個(gè)安全可靠的解決方案。
準(zhǔn)備好做些什么了嗎?現(xiàn)在開(kāi)始吧!
演示項(xiàng)目簡(jiǎn)介
使用LlamaIndex和Heroku學(xué)習(xí)RAG的最好方法是構(gòu)建一個(gè)小型的示例應(yīng)用程序。出于開(kāi)發(fā)人員的目的,假設(shè)正在與“古登堡計(jì)劃”(Project Gutenberg)合作,這是一個(gè)擁有70000多本免費(fèi)電子書(shū)的圖書(shū)館。如果想要構(gòu)建一個(gè)基于LLM的聊天機(jī)器人,它可以回答關(guān)于項(xiàng)目中免費(fèi)書(shū)籍的特定問(wèn)題。
這是使用RAG的完美用例,可以使用LlamaIndex獲得的大量書(shū)籍文本。為了使項(xiàng)目簡(jiǎn)單,將使用公元401年問(wèn)世的《圣奧古斯丁的懺悔錄》書(shū)中的內(nèi)容。
完成的項(xiàng)目代碼庫(kù)可以在這個(gè)GitHub存儲(chǔ)庫(kù)(https://github.com/alvinslee/llamaindex-gutenberg-demo)中找到。如果愿意的話,可以克隆repo并將應(yīng)用程序部署到Heroku?;蛘撸梢灾鸩搅私馊绾潍@得代碼。
構(gòu)建這個(gè)演示項(xiàng)目將遵循以下一些步驟:
- 設(shè)置項(xiàng)目。
- 加載數(shù)據(jù)。
- 構(gòu)建索引。
- 存儲(chǔ)索引。
- 集成LangChain。
- 部署到Heroku。
步驟1:設(shè)置項(xiàng)目
為Python項(xiàng)目創(chuàng)建一個(gè)新文件夾,然后激活venv并安裝需要的初始依賴項(xiàng)。
Shell
1 (venv) ~/project$ pip install llama-index langchain langchain-openai
接下來(lái),將加載要索引的數(shù)據(jù)。
步驟2:加載數(shù)據(jù)
在構(gòu)建用于RAG的內(nèi)部數(shù)據(jù)索引時(shí),必須將所有數(shù)據(jù)(文本)收集到一個(gè)地方。在這個(gè)例子中,該項(xiàng)目采用了《圣奧古斯丁的懺悔錄》的文本。將使用LlamaIndex將這個(gè)場(chǎng)景轉(zhuǎn)換為嵌入的矢量索引。
在典型的用例中,其場(chǎng)景數(shù)據(jù)將是適合試圖解決的業(yè)務(wù)問(wèn)題的大型文本語(yǔ)料庫(kù)。
對(duì)于這個(gè)小型演示項(xiàng)目,將創(chuàng)建一個(gè)名為data的子文件夾,然后將該書(shū)作為單個(gè)文件下載到該文件夾中。
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:構(gòu)建索引
在一個(gè)目錄中收集了所有數(shù)據(jù)之后,就可以構(gòu)建索引了。將編寫(xiě)一個(gè)簡(jiǎn)單的Python應(yīng)用程序,它將使用LlamaIndex為數(shù)據(jù)建立索引,然后查詢索引。
為此,需要一個(gè)OpenAI帳戶和API密鑰。這是因?yàn)長(zhǎng)lamaIndex使用OpenAI的text-embedding-3-small作為默認(rèn)嵌入模型 (更改這些默認(rèn)值超出了本文的討論范圍) 。
在這個(gè)項(xiàng)目的根文件夾中,創(chuàng)建了一個(gè)名為index.py的文件。初始內(nèi)容如下所示:
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)
運(yùn)行文件并收到預(yù)期的響應(yīng):
Shell
1 (venv) ~/project$ OPENAI_API_KEY=sk-******** python index.py
2 Hippo
當(dāng)然,可以再次檢查數(shù)據(jù)。看看這本書(shū)的前幾行,可以看到:
Shell
1 THE CONFESSIONS OF SAINT AUGUSTINE
2
3 By Saint Augustine
4
5 Bishop of Hippo
正如人們所看到的,LlamaIndex完成了它的工作。Python應(yīng)用程序完全按照開(kāi)發(fā)人員對(duì)向量索引數(shù)據(jù)的期望回答了問(wèn)題。
步驟4:存儲(chǔ)索引
需要注意的是,在上面的示例中,只將索引數(shù)據(jù)存儲(chǔ)在內(nèi)存中,而不是磁盤(pán)上。索引(現(xiàn)在是內(nèi)存中的一系列向量嵌入)將在調(diào)用OpenAI模型并完成工作流后完全丟失。
為文本創(chuàng)建向量索引(嵌入)不是免費(fèi)的,所以不想每次調(diào)用模型時(shí)都重新計(jì)算這些結(jié)果。最好有一個(gè)單獨(dú)的工作流將索引持久化到磁盤(pán)。然后,可以在任何時(shí)候引用它。
一種常見(jiàn)的方法是將嵌入存儲(chǔ)在PostgreSQL數(shù)據(jù)庫(kù)中,并使用pgvector執(zhí)行相似性搜索。為了使演示簡(jiǎn)單,只將索引數(shù)據(jù)存儲(chǔ)為平面文件。
因此,將這個(gè)簡(jiǎn)單的步驟添加到index.py文件中:
Python
1 PERSIST_DIR='./my_vector_indexes/gutenberg/'
2 index.storage_context.persist(persist_dir=PERSIST_DIR)
現(xiàn)在,在運(yùn)行文件之后,可以檢查存儲(chǔ)的索引。
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
已經(jīng)了解矢量索引存儲(chǔ)的基礎(chǔ)知識(shí),以及構(gòu)建一個(gè)矢量索引存儲(chǔ)是多么容易。但是,為了真正構(gòu)建一個(gè)將所有內(nèi)容鏈接在一起的端到端應(yīng)用程序,可以使用LangChain。這樣,就可以將解決方案部署為API??梢灾貙?xiě)index.py代碼,使其更適合生產(chǎn)環(huán)境。
以下將展示代碼,然后解釋接下來(lái)要做的事情。它可能看起來(lái)像很多代碼,但只添加了一些新步驟。
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)
首先,要注意的是,現(xiàn)在直接使用了LangChain和OpenAI。將LLM與一些內(nèi)存一起設(shè)置,以便在后續(xù)查詢中“記住”對(duì)話?,F(xiàn)在有一個(gè)真正的聊天機(jī)器人,可以與之互動(dòng)。
從這里,使用FastAPI創(chuàng)建一個(gè)API服務(wù)器,該服務(wù)器監(jiān)聽(tīng)/prompt端點(diǎn)上的POST請(qǐng)求。對(duì)該端點(diǎn)的請(qǐng)求預(yù)計(jì)具有帶有問(wèn)題的請(qǐng)求體,然后將其(連同來(lái)自向量索引的場(chǎng)景)傳遞給LLM。
使用uvicorn在端口8000上啟動(dòng)服務(wù)器。
在啟動(dòng)服務(wù)器之前,添加這些新的Python依賴項(xiàng):
Shell
1(venv) ~/project$ pip install fastapi pydantic uvicorn
現(xiàn)在是測(cè)試的時(shí)候了。首先啟動(dòng)服務(wù)器。
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)
在另一個(gè)終端中,向端點(diǎn)發(fā)送一個(gè)curl請(qǐng)求。
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
獲得成功!向量索引似乎已經(jīng)啟動(dòng)并運(yùn)行,這個(gè)聊天機(jī)器人功能齊全,是部署的時(shí)候了。
步驟6:部署到Heroku
在完成了主要的工作之后,只需要采取幾個(gè)簡(jiǎn)單的步驟將應(yīng)用程序部署到Heroku。
(1)用Python依賴項(xiàng)創(chuàng)建requirements.txt文件
Heroku需要知道在構(gòu)建項(xiàng)目時(shí)要安裝哪些Python依賴項(xiàng)。它在一個(gè)名為requirements.txt的文件中查找這個(gè)列表??梢杂孟旅娴拿钶p松地生成:
Shell
1(venv) ~/project$ pip freeze > requirements.txt
(2)創(chuàng)建Procfile
還需要告訴Heroku如何啟動(dòng)Python應(yīng)用程序。在一個(gè)名為Procfile的文件中執(zhí)行這一操作。
Shell
1 (venv) ~/project$ echo \
2 'web: uvicorn index:app --host=0.0.0.0 --port=${PORT}' > Procfile
(3)創(chuàng)建runtime.txt文件
最后,runtime.txt將告訴Heroku希望使用哪種Python運(yùn)行時(shí)版本。
Shell
1 (venv) ~/project$ echo 'python-3.11.8' > runtime.txt
這些都是需要的文件。這時(shí)項(xiàng)目文件夾結(jié)構(gòu)應(yīng)該看起來(lái)像(已經(jīng)刪除了持久化的矢量索引):
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
如果開(kāi)發(fā)人員是從頭開(kāi)始工作,并且沒(méi)有為這個(gè)演示項(xiàng)目克隆GitHub倉(cāng)庫(kù),那么將這些文件提交到自己的Git存儲(chǔ)庫(kù)。
(4)創(chuàng)建Heroku應(yīng)用程序
下載并安裝Heroku CLI后,執(zhí)行如下命令。開(kāi)發(fā)人員可以為其應(yīng)用程序選擇任何名字,需要提供唯一的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
部署應(yīng)用程序后,通過(guò)向API服務(wù)器發(fā)送curl請(qǐng)求進(jìn)行測(cè)試:
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調(diào)用在部署中使用了唯一的Heroku應(yīng)用URL。
現(xiàn)在已經(jīng)在Heroku上運(yùn)行了!
結(jié)論
現(xiàn)在已經(jīng)清楚地了解了LlamaIndex的強(qiáng)大功能,以及它在構(gòu)建RAG應(yīng)用程序與LLM交互時(shí)所扮演的重要角色。當(dāng)可以很容易地添加特定的數(shù)據(jù)源作為L(zhǎng)LM的場(chǎng)景,而不需采用成本昂貴的模型再訓(xùn)練時(shí),這是一個(gè)巨大的勝利。而對(duì)于希望進(jìn)一步推進(jìn)LLM工作流程的公司和開(kāi)發(fā)人員來(lái)說(shuō),這也是一個(gè)勝利。
將LlamaIndex與其他LangChain工具集結(jié)合起來(lái)也是無(wú)縫且直接的,構(gòu)建聊天機(jī)器人只需要幾行額外的代碼。最后,能夠快速輕松地將解決方案部署到Heroku,使應(yīng)用程序可以立即訪問(wèn),而不會(huì)有任何麻煩。像這樣的簡(jiǎn)單部署使開(kāi)發(fā)人員能夠?qū)W⒂跇?gòu)建基于LLM的解決方案這一更復(fù)雜、更重要的任務(wù)。
原文標(biāo)題:How To Implement RAG: A Simple Walkthrough,作者:Alvin Lee
鏈接:https://dzone.com/articles/how-to-implement-rag-a-simple-walkthrough。