譯者 | 布加迪
審校 | 重樓
“你能為我們開發(fā)一個聊天機(jī)器人嗎?” 如果你的IT團(tuán)隊還沒有收到這個請求,相信我,很快就會收到。隨著大語言模型(LLM)的興起,聊天機(jī)器人已成為新的必備功能——無論你是交付SaaS服務(wù)、管理內(nèi)部工具,還是僅僅試圖解讀龐大的文檔。問題是什么?僅僅將搜索索引粘貼到LLM上是不夠的。
如果你的聊天機(jī)器人需要從文檔、日志或其他內(nèi)部知識來源獲取答案,你不僅僅要構(gòu)建聊天機(jī)器人,還要構(gòu)建檢索管道。如果你不考慮數(shù)據(jù)的存儲位置、檢索方式以及遷移成本,你將面臨一個臃腫且脆弱的系統(tǒng)。
本文將詳細(xì)介紹如何構(gòu)建一個真正的對話式聊天機(jī)器人——它能夠利用檢索增強(qiáng)生成(RAG)技術(shù),盡量縮短延遲,并避開悄無聲息地扼殺利潤的云出站費(fèi)用陷阱。LLM是簡單的部分,基礎(chǔ)設(shè)施才是困難的部分,也是成本所在。
我將介紹一個簡單的對話式AI聊天機(jī)器人Web應(yīng)用程序,它有類似ChatGPT的UI,你可以輕松配置它,以便與OpenAI、DeepSeek 或任何其他大語言模型(LLM)配合使用。
第一部分:RAG 基礎(chǔ)知識
檢索增強(qiáng)生成(RAG)是一種將LLM的生成特性應(yīng)用于文檔集合的技術(shù),從而生成能夠根據(jù)文檔內(nèi)容有效回答問題的聊天機(jī)器人。
實現(xiàn)的典型RAG將集合中的每個文檔拆分成幾個大小大致相等且相互重疊的塊,并為每個塊生成嵌入(embedding)。嵌入是有成百上千個維度的浮點數(shù)向量(列表)。兩個向量之間的距離表示它們的相似度。距離小表示相似度高,距離大表示相似度低。
然后,RAG應(yīng)用程序將每個塊及其嵌入加載到向量存儲庫(vector storage)中。向量存儲庫是一個專用數(shù)據(jù)庫,可以執(zhí)行相似度搜索——給定一段文本,向量存儲庫就能通過比較嵌入來檢索按其與查詢文本的相似度排序的塊。
不妨將各部分整合起來:
獲得用戶提出的問題(1)時,RAG應(yīng)用程序可以查詢向量存儲庫,查找與問題(2)類似的文本塊。這些文本塊構(gòu)成了幫助LLM回答用戶問題的上下文。以下是使用文檔集合的示例:獲得問題“告訴我關(guān)于對象鎖的信息”時,向量存儲庫向應(yīng)用程序(3)返回四個文檔塊,每個塊約170個字。以下是每個塊的文本鏈接及簡短摘錄:
- 對象鎖:使用對象鎖合法保留功能,可以防止文件被更改或刪除;然而,不可變性沒有明確的保留期。
- 對象鎖:你可以對新創(chuàng)建的存儲桶或現(xiàn)有的存儲桶啟用對象鎖。
- 對象鎖:對象鎖在設(shè)計時已考慮到了Veeam Backup & Replication中的不可變性功能。
- 使用與S3兼容的API 啟用對象鎖:若要對現(xiàn)有的S3兼容存儲桶啟用對象鎖,請將值為1的x-amz-bucket-object-lock-token標(biāo)頭添加到S3 Put Object Lock Configuration API調(diào)用中。
不出所料,四個結(jié)果中有三個是來自關(guān)于對象鎖的主要文檔文章的摘錄。
為了提供對話體驗,RAG應(yīng)用程序在消息存儲庫中保存聊天歷史記錄。RAG應(yīng)用程序檢索消息歷史記錄(4),并將其作為查詢的一部分(將發(fā)送給LLM)。步驟2和步驟4(從向量存儲庫檢索數(shù)據(jù)和從消息存儲庫檢索數(shù)據(jù))可以同時執(zhí)行,因為它們彼此互不依賴。
系統(tǒng)提示為LLM生成答案設(shè)定了指導(dǎo)原則。在RAG中,系統(tǒng)提示通常規(guī)定答案應(yīng)該基于檢索到的文檔數(shù)據(jù),而非模型記憶,并嘗試通過確保模型在未找到相關(guān)文檔的情況下不會捏造信息來防止出現(xiàn)幻覺。以下是 RAG應(yīng)用程序中的系統(tǒng)提示:
Use the following pieces of context and the message history to answer the question at the end. If you don't know the answer, just say that you don't know; don't try to make up an answer.
RAG 將系統(tǒng)提示、上下文、對話中的最后幾條消息以及用戶的問題提交給LLM(5),LLM會生成一個但愿有所幫助的答案(6)。以下是它對上述問題的回答:
對象鎖是一項允許用戶強(qiáng)制云存儲中存儲的文件不可更改的功能。以下是關(guān)于對象鎖的幾個要點:
- 不可變性:文件鎖定后,在指定的保留期到期或合法保留解除之前,無法修改或刪除。
- 保留期:用戶可以在鎖定文件時設(shè)置保留期。在此期間,文件將保持不可變狀態(tài),這意味著文件無法更改或刪除。
- 合法保留:用戶可以將文件置于合法保留狀態(tài),這將無限期地阻止修改或刪除,直到保留被解除。
- 啟用對象鎖:可以為新存儲桶或現(xiàn)有存儲桶啟用對象鎖,但必須在上傳文件之前在存儲桶層面加以啟用。
- 集成:一些應(yīng)用程序(比如Veeam Backup和Replication)內(nèi)置了對象鎖支持,方便了希望確保備份期間文件不可變的用戶。
- 無需額外費(fèi)用:使用對象鎖無需額外費(fèi)用,然而鎖定的文件仍需支付標(biāo)準(zhǔn)的存儲費(fèi)用。
- 鎖定錯誤:如果用戶鎖定文件的時間超過預(yù)期,可能需要聯(lián)系支持人員以解決問題,在某些情況下,這可能需要關(guān)閉存儲帳戶。
總之,對于需要確保數(shù)據(jù)完整性并遵守數(shù)據(jù)保留相關(guān)法規(guī)的組織而言,對象鎖是一項至關(guān)重要的功能。
RAG應(yīng)用程序將用戶的問題和LLM的回復(fù)添加到消息存儲庫(7),將答案返回給用戶(8),然后等待下一個問題。
示例應(yīng)用程序一覽
示例應(yīng)用程序已發(fā)布在GitHub上。該應(yīng)用程序是開源的,采用MIT許可證,因此你可以不受任何限制地將其用于試驗。該應(yīng)用程序使用與S3兼容的API,因此它可以與任何與S3兼容的對象存儲庫兼容。
請注意,與任何與一個或多個云服務(wù)提供商(CSP)集成的示例應(yīng)用程序一樣,運(yùn)行該示例應(yīng)用程序時可能會產(chǎn)生費(fèi)用,包括存儲數(shù)據(jù)和從CSP下載數(shù)據(jù)的費(fèi)用。下載費(fèi)用通常名為“出站費(fèi)”,可能很快就會超過存儲數(shù)據(jù)的費(fèi)用。AI應(yīng)用程序通常會集成多家專業(yè)提供商的功能,因此你應(yīng)該仔細(xì)檢查云存儲提供商的定價,免得月底收到賬單時大吃一驚。貨比三家,幾家專業(yè)的云存儲提供商提供慷慨的每月免費(fèi)出站流量限額,最高可達(dá)存儲數(shù)據(jù)量的三倍,有的存儲提供商還為合作伙伴提供無限量的免費(fèi)出站流量。
README文件詳細(xì)介紹了配置和部署;我在本文中將作一概述。該示例應(yīng)用程序使用 Python和Django Web框架編寫而成。API憑據(jù)和相關(guān)設(shè)置通過環(huán)境變量來配置,而LLM和向量存儲庫通過Django的settings.py文件來配置:
CHAT_MODEL: ModelSpec = {
'name': 'OpenAI',
'llm': {
'cls': ChatOpenAI,
'init_args': {
'model': "gpt-4o-mini",
}
},
}
# Change source_data_location and vector_store_location to match your environment
# search_k is the number of results to return when searching the vector store
DOCUMENT_COLLECTION: CollectionSpec = {
'name': 'Docs',
'source_data_location': 's3://rag-app-bucket/pdfs',
'vector_store_location': 's3://rag-app-bucket/vectordb/docs/openai',
'search_k': 4,
'embeddings': {
'cls': OpenAIEmbeddings,
'init_args': {
'model': "text-embedding-3-large",
},
},
}
示例應(yīng)用程序經(jīng)配置后,使用OpenAI GPT-4o mini。不過,README文件解釋了如何通過Ollama框架使用不同的在線LLM,比如DeepSeek V3 或Google Gemini 2.0 Flash,甚至像Meta Llama 3.1這樣的本地LLM。如果你確實運(yùn)行本地LLM,務(wù)必選擇適合你硬件的模型。我嘗試在搭載M1 Pro CPU的MacBook Pro上運(yùn)行Meta的Llama 3.3,它有700億個參數(shù)(70B)。僅僅回答一個問題花了將近3個小時!Llama 3.1 8B適合得多,不到30秒就能回答問題。
請注意,文檔集合配置了向量存儲庫的位置,該存儲庫包含技術(shù)文檔庫作為示例數(shù)據(jù)集。README文件包含一個應(yīng)用程序密鑰,該密鑰對PDF和向量存儲庫擁有只讀訪問權(quán)限,因此你無需加載自己的文檔集即可試用該應(yīng)用程序。
如果你想使用文檔集合,一對自定義命令允許你將它們從云對象存儲加載到向量存儲庫中,然后查詢向量存儲庫以測試一切是否正常。
首先,你需要加載數(shù)據(jù):
% python manage.py load_vector_store
Deleting existing LanceDB vector store at s3://rag-app-bucket/vectordb/docs
Creating LanceDB vector store at s3://rag-app-bucket/vectordb/docs
Loading data from s3://rag-app-bucket/pdfs in pages of 1000 results
Successfully retrieved page 1 containing 618 result(s) from s3://rag-app-bucket/pdfs
Skipping pdfs/.bzEmpty
Skipping pdfs/cloud_storage/.bzEmpty
Loading pdfs/cloud_storage/cloud-storage-add-file-information-with-the-native-api.pdf
Loading pdfs/cloud_storage/cloud-storage-additional-resources.pdf
Loading pdfs/cloud_storage/cloud-storage-api-operations.pdf
...
Loading pdfs/v1_api/s3-put-object.pdf
Loading pdfs/v1_api/s3-upload-part-copy.pdf
Loading pdfs/v1_api/s3-upload-part.pdf
Loaded batch of 614 document(s) from page
Split batch into 2758 chunks
[2025-02-28T01:26:11Z WARN lance_table::io::commit] Using unsafe commit handler. Concurrent writes may result in data loss. Consider providing a commit handler that prevents conflicting writes.
Added chunks to vector store
Added 614 document(s) containing 2758 chunks to vector store; skipped 4 result(s).
Created LanceDB vector store at s3://rag-app-bucket/vectordb/docs. "vectorstore" table contains 2758 rows
不要被“不安全的提交處理程序”警告嚇倒,我們的示例向量存儲庫永遠(yuǎn)不會接收并發(fā)寫入,因此不會發(fā)生沖突或數(shù)據(jù)丟失。
現(xiàn)在,你可以通過查詢向量存儲庫來驗證數(shù)據(jù)是否已存儲。請注意來自向量存儲庫的原始結(jié)果包含一個標(biāo)識源文檔的S3 URI:
% python manage.py search_vector_store 'Which S3 API operation would I use to upload a file?'
2025-04-07 16:24:51,615 ai_rag_app.management.commands.search_vector_store INFO Opening vector store at s3://blze-ev-ai-rag-app/vectordb/docs/openai
2025-04-07 16:24:51,615 ai_rag_app.utils.vectorstore DEBUG Populating AWS environment variables from the b2-ev profile
Found 4 docs in 5.25 seconds
2025-04-07 16:24:57,386 ai_rag_app.management.commands.search_vector_store INFO
page_cnotallow='b2_list_parts b2_list_unfinished_large_files b2_start_large_file b2_update_file_legal_hold b2_update_bucket b2_upload_file b2_update_file_retention b2_upload_part S3-Compatible API To go directly to the detailed S3-Compatible API operations, click here. To learn more about using the S3-Compatible API, click here. API Operations Object Operations S3 Copy Object S3 Delete Object S3 Get Object S3 Get Object ACL S3 Get Object Legal Hold S3 Get Object Retention S3 Head Object S3 Put Object S3 Put Object ACL S3 Put Object Legal Hold S3 Put Object Retention S3 Abort Multipart Upload S3 Complete Multipart Upload S3 Create Multipart Upload S3 Upload Part S3 Upload Part Copy S3 List Multipart Uploads Bucket Operations S3 Create Bucket S3 Delete Bucket S3 Delete Bucket CORS S3 Delete Bucket Encryption S3 Delete Objects S3 Get Bucket ACL S3 Get Bucket CORS S3 Get Bucket Encryption S3 Get Bucket Location S3 Get Bucket Versioning' metadata={'source': 's3://blze-ev-ai-rag-app/pdfs/cloud_storage/cloud-storage-api-operations.pdf'}
...
示例應(yīng)用程序的核心是RAG類。幾種方法可以創(chuàng)建RAG的基本組件,但在這里我們將介紹_create_chain() 方法如何使用開源LangChain AI框架,將系統(tǒng)提示、向量存儲庫、消息歷史記錄和LLM整合在一起。
首先,我們定義系統(tǒng)提示,包含上下文的占位符——RAG 將從向量存儲庫檢索的那些文本塊:
# These are the basic instructions for the LLM
system_prompt = (
"Use the following pieces of context and the message history to "
"Answer the question at the end. If you don't know the answer, "
"just say that you don't know, don't try to make up an answer. "
"\n\n"
"Context: {context}"
)
然后,我們創(chuàng)建一個提示模板,將系統(tǒng)提示、消息歷史記錄和用戶問題組合在一起:
# The prompt template brings together the system prompt, context, message history and the user's question
prompt_template = ChatPromptTemplate(
[
("system", system_prompt),
MessagesPlaceholder(variable_name="history", optinotallow=True, n_messages=10),
("human", "{question}"),
]
)
現(xiàn)在,我們使用LangChain表達(dá)式語言(LCEL)將各個組件組成一個鏈。LCEL允許我們以聲明式的方式定義組件鏈;也就是說,我們提供所需鏈的大體表示,而不是指定組件應(yīng)如何鏈接在一起:
# Create the basic chain
# When loglevel is set to DEBUG, log_input will log the results from the vector store
chain = (
{
"context": (
itemgetter("question")
| retriever
| log_data('Documents from vector store', pretty=True)
),
"question": itemgetter("question"),
"history": itemgetter("history"),
}
| prompt_template
| model
| log_data('Output from model', pretty=True)
)
注意log_data()輔助方法,它僅僅記錄其輸入數(shù)據(jù),并將其傳遞給鏈中的下一個組件。
為鏈分配名稱使我們能夠在調(diào)用它時添加檢測機(jī)制。你將在本文后面看到我們?nèi)绾翁砑右粋€回調(diào)處理程序,該處理程序?qū)?zhí)行該鏈所花費(fèi)的時間注釋到鏈的輸出中:
# Give the chain a name so the handler can see it
named_chain: Runnable[Input, Output] = chain.with_config(run_name="my_chain")
現(xiàn)在,我們使用LangChain的RunnableWithMessageHistory類來管理從消息存儲庫添加和檢索消息。Django框架為每個用戶分配會話ID,我們使用它作為存儲和檢索消息歷史記錄的鍵:
# Add message history management
return RunnableWithMessageHistory(
named_chain,
lambda session_id: RAG._get_session_history(store, session_id),
input_messages_key="question",
history_messages_key="history",
)
最后,log_chain()函數(shù)將鏈的ASCII表示打印輸出到調(diào)試日志中。請注意,即使不使用session_id,我們也必須提供預(yù)期的配置:
log_chain(history_chain, logging.DEBUG, {"configurable": {'session_id': 'dummy'}})
這是輸出——它提供了直觀的圖示,表明了數(shù)據(jù)如何在鏈中流動:
轉(zhuǎn)儲器組件由log_data()輔助方法插入,用于將沿鏈傳遞的數(shù)據(jù)記入日志。相比之下,Lambda組件由itemgetter()方法插入,用于從傳入的Python字典中提取元素。
RAG類的invoke()函數(shù)用于響應(yīng)用戶的問題,非常簡單。以下是代碼的關(guān)鍵部分:
response = self._chain.invoke(
{"question": question},
cnotallow={
"configurable": {
"session_id": session_key
},
"callbacks": [
ChainElapsedTime("my_chain")
]
},
)
鏈的輸入是一個包含問題的Python字典,而config參數(shù)使用Django會話密鑰和回調(diào)函數(shù)配置鏈,該回調(diào)函數(shù)用執(zhí)行時間注釋鏈的輸出。由于鏈輸出包含Markdown格式,因此處理來自前端請求的API端點使用開源markdown-it庫將輸出渲染為HTML以供顯示。
其余代碼主要涉及渲染W(wǎng)eb UI。一個有趣的方面是,負(fù)責(zé)在頁面加載時渲染UI的 Django視圖使用RAG的消息存儲庫來渲染對話,因此如果你重新加載頁面,也不會丟失上下文。
運(yùn)行此代碼!
如上所述,示例AI RAG應(yīng)用程序是開源的,采用MIT許可證,我鼓勵你將其用來探索RAG。README文件建議了幾種擴(kuò)展方法,如果你考慮在生產(chǎn)環(huán)境中運(yùn)行該應(yīng)用程序,也請你注意README的結(jié)尾部分:
[…]為了讓你快速上手,我們通過幾種方式簡化了應(yīng)用程序。如果你希望在生產(chǎn)環(huán)境中運(yùn)行該應(yīng)用程序,需要注意以下幾點:
- 該應(yīng)用程序不使用數(shù)據(jù)庫來存儲用戶帳戶或任何其他數(shù)據(jù),因此無需身份驗證。所有訪問都是匿名的。如果你希望用戶登錄,則需要將Django的AuthenticationMiddleware類恢復(fù)到MIDDLEWARE配置,并配置數(shù)據(jù)庫。
- 會話存儲在內(nèi)存中。如上所述,你可以使用Gunicorn將應(yīng)用程序擴(kuò)展為多線程,但你需要配置Django會話后端才能在多個進(jìn)程中或多個主機(jī)上運(yùn)行應(yīng)用程序。
- 同樣,對話歷史記錄存儲在內(nèi)存中,因此你需要使用持久化消息歷史記錄實現(xiàn)機(jī)制(比如 RedisChatMessageHistory)才能在多個進(jìn)程中或多個主機(jī)上運(yùn)行應(yīng)用程序。
最重要的是,玩得開心!AI是一項迅猛發(fā)展的技術(shù),廠商和開源項目每天都在發(fā)布新功能。但愿你覺得這個應(yīng)用程序是不錯的入門工具。
原文標(biāo)題:A Practical Guide To Building a RAG-Powered Chatbot,作者:Pat Patterson