)
上周有個(gè)朋友問我公司里攢了幾百份產(chǎn)品文檔、報(bào)銷制度、設(shè)備操作手冊散落在各個(gè)文件夾里新同事想問點(diǎn)東西只能靠人肉搜索。他想做一個(gè)“文檔問答機(jī)器人”但一搜教程全是講微調(diào)大模型、租GPU訓(xùn)練的那種看起來就勸退。我直接給他推了另一條路LangChain 加通義千問做一套 RAG 知識庫問答系統(tǒng)半小時(shí)就能把鏈路跑通。這篇文章就是把當(dāng)時(shí)那套可運(yùn)行的代碼完整拆開講清楚末尾附了完整版復(fù)制下來換個(gè) API Key 就能用。這篇文章適合兩類人一是想給自己的資料庫做智能問答、但不想碰模型訓(xùn)練的開發(fā)者二是剛接觸 RAG、想找一個(gè)最小可運(yùn)行 Demo 來理解全流程的學(xué)習(xí)者。我不會只丟代碼給你我會把文檔加載、文本切分、向量化、檢索、Prompt 設(shè)計(jì)這些環(huán)節(jié)的“為什么”都講明白。看完你會發(fā)現(xiàn)RAG 這套東西原理上就是一層窗戶紙真正的坑全在細(xì)節(jié)里。1. RAG到底在解決什么問題1.1 大模型天生回答不了私有知識先想一個(gè)場景你直接問一個(gè)通用大模型“我們公司的請假審批流程是什么”它能答嗎大概率是答不了的。因?yàn)槟P驮谟?xùn)練時(shí)只見過公開的互聯(lián)網(wǎng)語料它根本沒見過你們公司的內(nèi)部制度。這時(shí)候它又不會主動說“我不知道”而是會根據(jù)自己理解過的類似內(nèi)容編造一套“看起來很像回事”的流程給你。這就是行業(yè)里常說的“幻覺”。這個(gè)問題不是換一個(gè)更大的模型就能解決的。你再大的模型它的知識截止時(shí)間也是固定的你們的文檔今天更新了它不可能今天就學(xué)會。私有知識、企業(yè)內(nèi)部資料、最新的產(chǎn)品變更說明這些東西天然不在模型的訓(xùn)練范圍里。所以大模型落地到具體業(yè)務(wù)場景時(shí)第一個(gè)要解決的問題不是“模型不夠聰明”而是“模型看不到該看的數(shù)據(jù)”。1.2 RAG的三段式流水線RAG 的全稱是 Retrieval-Augmented Generation檢索增強(qiáng)生成。這個(gè)名字很學(xué)術(shù)但背后邏輯特別樸素——與其強(qiáng)迫模型記住所有知識不如在它回答問題之前先把相關(guān)的資料找出來遞給它讓它“看著資料回答”。整個(gè)過程分三段第一段是索引構(gòu)建。把散落在 txt、docx、pdf 里的文檔全部讀取進(jìn)來切成一個(gè)個(gè)幾百字的文本塊再用 Embedding 模型把每個(gè)文本塊變成一組向量最后存進(jìn)向量數(shù)據(jù)庫。這一步是離線完成的跑一次就行。第二段是檢索。用戶輸入一個(gè)問題時(shí)系統(tǒng)先把問題用同一個(gè) Embedding 模型變成向量然后在向量庫里做相似度搜索找出和這個(gè)問題最相關(guān)的 3 到 5 個(gè)文本塊。這一步是實(shí)時(shí)的要求快。第三段是生成。把檢索到的文本塊和用戶問題拼成一個(gè) Prompt交給大模型讓它基于這些檢索到的資料作答。這一步很像開卷考試模型不用背全部內(nèi)容但它手邊有一份參考資料回答必須從資料里找依據(jù)。如果你想一句話記牢RAG 就是用“開卷考試”的方式替代“閉卷硬背”的傳統(tǒng)問答。普通 LLM 問答是閉卷RAG 是讓模型帶著參考資料進(jìn)考場。1.3 為什么我選擇RAG而不是微調(diào)很多第一次接觸知識庫問答的人會問為什么不微調(diào)模型微調(diào)確實(shí)可以把特定知識“訓(xùn)練進(jìn)”模型參數(shù)里但對大多數(shù)知識庫場景來說RAG 的性價(jià)比遠(yuǎn)高于微調(diào)。我拿常用的幾個(gè)維度做個(gè)對比對比項(xiàng)微調(diào)RAG數(shù)據(jù)準(zhǔn)備需要標(biāo)注好的“問題-答案”訓(xùn)練數(shù)據(jù)只需要原始文檔零標(biāo)注硬件成本需要 GPU 訓(xùn)練周期以天計(jì)普通 CPU 就能完成索引分鐘級知識更新文檔一變重新訓(xùn)練一版換文檔重跑一次索引即可可解釋性黑盒答案無法溯源能返回命中的原文片段對幻覺的抑制依然可能編造配合提示詞約束大幅降低編造你可以把微調(diào)和 RAG 的關(guān)系理解為“背教材”和“查教材”。如果你要的是學(xué)生開口就是專業(yè)術(shù)語、語氣完全貼合你的團(tuán)隊(duì)風(fēng)格那微調(diào)合適。但如果你本質(zhì)上是想解決“內(nèi)部資料太多了大家找不到答案”的問題那就不要去訓(xùn)練模型而是把檢索做到位。甚至很多成熟項(xiàng)目是先用 RAG 把知識檢索鏈路跑通后續(xù)再考慮用微調(diào)優(yōu)化生成風(fēng)格兩者并不是互斥的關(guān)系。2. 技術(shù)選型為什么是LangChain、通義千問、FAISS2.1 LangChain到底幫你省了什么沒有 LangChain 的時(shí)候做一個(gè) RAG 系統(tǒng)要自己處理的事情非常多寫代碼遍歷目錄加載文檔處理不同文件格式的解析邏輯自己實(shí)現(xiàn)文本切分算法對接向量數(shù)據(jù)庫的 API還要寫 Prompt 拼接和結(jié)果解析。每一步都不難但堆在一起就很煩。LangChain 的核心價(jià)值是流程編排和組件標(biāo)準(zhǔn)化。它把 RAG 拆成一個(gè)個(gè)標(biāo)準(zhǔn)組件文檔加載器負(fù)責(zé)讀取文件、文本分割器負(fù)責(zé)切分、Embedding 封裝負(fù)責(zé)調(diào)向量模型、向量存儲封裝負(fù)責(zé)讀寫向量庫、鏈Chain負(fù)責(zé)把檢索結(jié)果拼進(jìn) Prompt 再調(diào)大模型。你要做的事情從“寫全部代碼”變成了“選組件、配參數(shù)”。當(dāng)然LangChain 也是被吐槽最多的框架之一因?yàn)樗骂l繁、抽象層級多、版本之間破壞性改動不少。但即便如此我依然推薦在起步階段用它。原因很簡單RAG 的鏈路很固定你沒必要自己造每一塊輪子。LangChain 幫你把邊界和接口定義好了等以后你真要上生產(chǎn)再針對瓶頸替換具體實(shí)現(xiàn)也不遲。2.2 通義千問作為大模型接口大模型選型我選的是通義千問代碼里用的是 ChatTongyi 這個(gè)封裝。選它的理由主要有三個(gè)。第一中文效果好。知識庫問答場景里檢索到的資料和用戶的問題大部分是中文模型對中文語義的理解能力直接決定輸出質(zhì)量。通義千問的中文能力在實(shí)測中表現(xiàn)穩(wěn)定尤其是指令跟隨和長文本歸納這兩塊。第二接入成本低。LangChain 社區(qū)里已經(jīng)有現(xiàn)成的 ChatTongyi 類填一個(gè) API Key 和模型名就能用不需要自己寫 SDK 調(diào)用邏輯。如果你以后想換別的國產(chǎn)模型也可以看 LangChain 是否支持對應(yīng)的 Chat 類接口風(fēng)格基本一致。第三費(fèi)用可控。它有不同檔位的模型日常測試用輕量型號跑通了再升級到更強(qiáng)型號切換成本幾乎為零。對我這種經(jīng)常要寫 Demo 驗(yàn)證想法的人來說這個(gè)體驗(yàn)很重要。2.3 Embedding模型與向量庫的選擇RAG 里有兩個(gè)“向量”相關(guān)的東西容易被混為一談一個(gè)是把文本變成向量的 Embedding 模型一個(gè)是存向量的向量數(shù)據(jù)庫。兩者都影響效果但 Embedding 模型的影響更大——檢索找不到相關(guān)內(nèi)容后面生成再好也是巧婦難為無米之炊。Embedding 我這里用的是 text-embedding-v3通義千問配套的中文向量模型。中文場景下用專門優(yōu)化過中文語料的 Embedding 模型比直接用英文模型的效果好很多。這個(gè)差別在短文本檢索時(shí)特別明顯。向量庫我選的是 FAISS而不是直接上 Milvus 或 Elasticsearch。原因很實(shí)在FAISS 是一個(gè)本地計(jì)算庫pip install faiss-cpu就能用不需要單獨(dú)啟動服務(wù)不需要運(yùn)維特別適合個(gè)人項(xiàng)目和小規(guī)模知識庫。你的知識庫如果只是幾千個(gè)文本塊FAISS 的檢索性能綽綽有余。真到了百萬級數(shù)據(jù)量再遷移到專用向量數(shù)據(jù)庫LangChain 的接口層基本不用動這點(diǎn)也是框架帶來的好處。3. 環(huán)境準(zhǔn)備先把地基打好3.1 用虛擬環(huán)境隔離項(xiàng)目依賴開始寫代碼之前我強(qiáng)烈建議先建一個(gè)獨(dú)立的 Python 虛擬環(huán)境。LangChain 的依賴樹比一般庫要復(fù)雜它底層依賴 pydantic而 pydantic 對版本要求非常嚴(yán)格。如果你把 LangChain 直接裝進(jìn)全局環(huán)境大概率會跟其它項(xiàng)目里固定版本的 pydantic 起沖突到時(shí)候滿屏報(bào)錯查得頭疼。創(chuàng)建命令很簡單把這段拿過去用就行python -m venv rag_env # Windows 激活 rag_env\Scripts\activate # macOS / Linux 激活 source rag_env/bin/activatePython 版本建議用 3.10 或 3.11。3.8 太舊個(gè)別新版依賴已經(jīng)不再支持3.12 也能跑但有些包在 Windows 上可能沒有預(yù)編譯的 wheel裝起來麻煩。我自己的項(xiàng)目環(huán)境是 Python 3.11實(shí)測最穩(wěn)。3.2 核心依賴安裝清單這是一個(gè)最小可運(yùn)行方案不需要裝一大堆用不上的包。我在項(xiàng)目里實(shí)際安裝的依賴就這幾個(gè)pip install langchain langchain-community langchain-text-splitters pip install faiss-cpu pip install dashscope python-dotenv逐個(gè)說下它們是干嘛的。langchain主框架提供鏈、Prompt 模板這些核心能力。langchain-community第三方集成組件的家。我們后面要用的文檔加載器、FAISS 向量庫、ChatTongyi、DashScopeEmbeddings 都在這個(gè)包里。新版 LangChain 把社區(qū)集成從主包里拆出來了所以必須單獨(dú)裝。langchain-text-splitters文本切分工具。這個(gè)也被拆成了獨(dú)立包專門放 RecursiveCharacterTextSplitter。faiss-cpuFAISS 的 CPU 版本。個(gè)人和小型知識庫場景下CPU 版本和 GPU 版本在檢索速度上的差距你幾乎感知不到裝 CPU 版最省事。dashscope通義千問模型的官方 SDK。LangChain 的 ChatTongyi 內(nèi)部會調(diào)用它所以這也是隱性依賴。python-dotenv用來讀取 .env 文件里的環(huán)境變量方便管理 API Key。如果你的知識文檔里有 PDF再額外裝一個(gè)pip install pypdf。這是 PyPDFLoader 依賴的后端不裝的話加載 PDF 會直接報(bào)錯。3.3 API Key 配置的正確姿勢通義千問的模型服務(wù)是走 API 調(diào)用的你需要先去模型服務(wù)控制臺開通服務(wù)然后創(chuàng)建一個(gè) API Key。過程不復(fù)雜重點(diǎn)是拿到 Key 之后怎么管理。不要在代碼里硬編碼 API Key。很多人圖快直接把 Key 寫在 main.py 里然后整個(gè)文件夾傳到 Git 倉庫Key 就泄露了。正確做法是在項(xiàng)目根目錄建一個(gè) .env 文件DASHSCOPE_API_KEY你的key代碼里通過load_dotenv()自動讀取然后os.getenv(DASHSCOPE_API_KEY)拿到值。.env 文件記得加進(jìn) .gitignore永遠(yuǎn)不要提交到遠(yuǎn)程倉庫。代碼里還有一個(gè)健壯性細(xì)節(jié)如果 API Key 為空直接拋異??焖偈《皇亲尦绦蚺艿揭话氩乓?yàn)檎J(rèn)證失敗報(bào)錯。這個(gè)小習(xí)慣能省很多調(diào)試時(shí)間。4. 核心代碼拆解一條鏈看懂 RAG 全流程4.1 文檔加載把磁盤上的文件變成 DocumentRAG 的第一步是把文檔讀進(jìn)來。我用的加載器是 DirectoryLoader 加 TextLoader 的組合loader DirectoryLoader( ./knowledge, glob**/*.txt, loader_clsTextLoader, loader_kwargs{encoding: utf-8}, ) documents loader.load()DirectoryLoader 負(fù)責(zé)遍歷一個(gè)目錄glob**/*.txt表示匹配目錄下所有子文件夾里的 txt 文件。loader_clsTextLoader指定具體用 TextLoader 去解析文件內(nèi)容。load() 返回的是一個(gè) Document 對象列表。每個(gè) Document 有兩個(gè)核心字段page_content是正文文本metadata是文件信息比如文件路徑。這個(gè) metadata 后面會當(dāng)答案來源展示用非常關(guān)鍵。單獨(dú)說明一下編碼問題。TextLoader 默認(rèn)按 utf-8 讀取文件但很多人電腦上的 txt 是 GBK 編碼結(jié)果一運(yùn)行就拋 UnicodeDecodeError。所以我這里用 loader_kwargs 顯式傳了encodingutf-8。如果你手上的文檔是 GBK就把值改成gbk。最省心的做法是統(tǒng)一把知識庫文檔轉(zhuǎn)成 utf-8之后就沒這個(gè)煩惱了。4.2 文本切分控制每個(gè)片段的信息量文檔加載進(jìn)來后不能直接向量化因?yàn)檎臋n太長了。一篇 3000 字的說明書如果整體變成一個(gè)向量向量里各個(gè)主題的信息會互相干擾檢索時(shí)相關(guān)性會被稀釋。所以要先切塊。text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, ) docs text_splitter.split_documents(documents)這里有兩個(gè)參數(shù)需要重點(diǎn)理解chunk_size 和 chunk_overlap。chunk_size500 表示每個(gè)文本塊大約 500 個(gè)字符。為什么選 500因?yàn)橹形膱鼍跋乱粋€(gè)漢字差不多對應(yīng)一個(gè) token500 個(gè)字符就是大概 500 個(gè) token 的文本既能承載一個(gè)相對完整的知識點(diǎn)又不會占用太多大模型上下文窗口。如果設(shè)成 2000一個(gè)塊里塞了好幾個(gè)主題檢索出來的相關(guān)性就不精確如果設(shè)成 100每段內(nèi)容太短模型拿不到完整上下文回答容易斷章取義。chunk_overlap50 表示相鄰兩個(gè)塊之間保留 50 個(gè)字符的重疊。這 50 個(gè)字符是為了防止切分邊界恰好把一段完整的話攔腰截?cái)?。比如上一塊結(jié)尾在講某個(gè)概念的定義下一塊開頭也是這個(gè)概念重疊可以讓兩塊各自都保留完整信息。RecursiveCharacterTextSplitter 的切分邏輯不是簡單地按長度硬切而是優(yōu)先按換行符、句號、空格這樣的天然邊界遞歸切分。切出來都是相對完整的句子比無腦按字符切要智能一點(diǎn)。4.3 向量化與入庫構(gòu)建一次反復(fù)使用文本切好后下一步就是向量化并存入向量庫。這一步在我的代碼里是兩行embeddings DashScopeEmbeddings(modeltext-embedding-v3) vectorstore FAISS.from_documents(docs, embeddings) vectorstore.save_local(./faiss_index)第一行創(chuàng)建 Embedding 模型實(shí)例。第二行對每個(gè)文本塊調(diào)用模型接口生成向量并寫入 FAISS 索引。第三行把索引持久化到本地磁盤。FAISS.from_documents 是同步流程文檔多的時(shí)候這里會明顯卡幾十秒是正常的它在挨個(gè)調(diào)用 Embedding API。有一個(gè)經(jīng)驗(yàn)值得記下來如果只是改動 Prompt 模板不需要重建索引如果你改了切分策略、換了 Embedding 模型或者新增了大量文檔就建議重新 build 一次索引。因?yàn)樗饕锎娴氖俏谋緣K的向量Prompt 改了不影響向量內(nèi)容。4.4 檢索器與Prompt設(shè)計(jì)決定回答質(zhì)量的兩只手索引構(gòu)建好之后要把它包裝成檢索器retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 4}, )search_typesimilarity表示用向量余弦相似度找最相關(guān)的文本塊。k4表示取最相關(guān)的 4 塊。這個(gè)數(shù)字不是拍腦袋定的太小了可能漏掉關(guān)鍵信息太大了又可能把一堆不太相關(guān)的內(nèi)容也塞進(jìn)上下文反而干擾模型。4 這個(gè)值在大多數(shù)場景下是個(gè)不錯的起點(diǎn)你可以在測試時(shí)調(diào)到 3 或 5 對比效果。Prompt 模板是整個(gè)系統(tǒng)里最直接的“指揮棒”我用的模板是prompt_template 請基于以下給定的資料用簡潔、專業(yè)、準(zhǔn)確的中文回答用戶的問題。 如果給定的資料中沒有相關(guān)內(nèi)容請直接回答根據(jù)現(xiàn)有知識無法回答該問題。禁止編造或臆測。 給定資料 {context} 用戶問題 {question} 你的回答這個(gè)模板的重點(diǎn)在于“禁止編造”四個(gè)字。RAG 系統(tǒng)的幻覺問題不只在檢索環(huán)節(jié)如果 Prompt 沒有明確約束模型會把檢索到的資料和自己記憶里的舊知識混在一起作答。給它設(shè)置一個(gè)“不知道就直說”的兜底是抑制幻覺最簡單有效的一招。模板里的{context}和{question}是占位符組裝時(shí)會分別替換成檢索到的文本塊和用戶問題。4.5 組裝問答鏈并調(diào)用組裝用的是 RetrievalQA 這個(gè)便捷鏈qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, retrieverretriever, return_source_documentsTrue, chain_type_kwargs{prompt: QA_CHAIN_PROMPT}, ) result qa_chain.invoke({query: 報(bào)銷流程是什么})chain_typestuff表示把檢索到的所有文本塊直接拼接后塞進(jìn) Prompt。這是最簡單也最推薦的方式因?yàn)樗讶亩冀o了模型信息損失最小。只有當(dāng)你檢索結(jié)果特別多、超過模型上下文限制時(shí)才需要考慮 map_reduce 或 refine 這樣的復(fù)雜策略。調(diào)用時(shí)輸入的是一個(gè)字典key 是query。這也是 RetrievalQA 隱式的約定很多人第一次用會慣性寫成question結(jié)果跑起來直接報(bào)錯。輸出 result 是一個(gè)字典result[result]是模型生成的最終回答result[source_documents]是命中的文本塊列表遍歷它就能拿到每個(gè)塊的原文和文件路徑。調(diào)試時(shí)這個(gè)字段特別重要它能幫你看到模型到底依據(jù)了哪些資料。5. 實(shí)測中的高頻坑與排查思路5.1 中文編碼引發(fā) UnicodeDecodeError這是最常見的啟動報(bào)錯。TextLoader 默認(rèn)用 utf-8 讀文件但你的 txt 可能是 GBK 編碼一加載就拋 UnicodeDecodeError。我的處理方式是在 DirectoryLoader 里顯式傳loader_kwargs{encoding: utf-8}然后把知識庫原始文檔統(tǒng)一用記事本或腳本轉(zhuǎn)成 UTF-8。手動改文件編碼太累了可以在代碼里加個(gè)預(yù)處理或者干脆要求團(tuán)隊(duì)上傳文檔時(shí)統(tǒng)一格式。如果你堅(jiān)持讀 GBK就把 encoding 改成gbk。5.2 FAISS加載報(bào)錯allow_dangerous_deserialization新版本 LangChain 在加載本地 FAISS 索引時(shí)強(qiáng)制要求你顯式聲明允許反序列化否則會報(bào)一堆安全提示。這是 because FAISS 的本地索引是用 pickle 序列化的反序列化惡意文件可能有安全風(fēng)險(xiǎn)。解決辦法很簡單加載時(shí)加上參數(shù)vectorstore FAISS.load_local( ./faiss_index, embeddings, allow_dangerous_deserializationTrue, )這個(gè)參數(shù)看著嚇人但只要你加載的是自己生成的索引文件在本地可信環(huán)境里跑沒有實(shí)際問題。5.3 模型調(diào)用失敗限流、超時(shí)與Key配置如果你在循環(huán)測試時(shí)連續(xù)提問可能會碰到服務(wù)端返回限流錯誤或超時(shí)。第一反應(yīng)先檢查幾個(gè)點(diǎn)API Key 是否配置在環(huán)境變量里DASHSCOPE_API_KEY 是否真的讀到了模型名是否拼寫正確網(wǎng)絡(luò)環(huán)境是否穩(wěn)定。排查完了如果還報(bào)限流最簡單的方案是把代碼里的模型從效果更強(qiáng)的型號換成輕量型號。日常功能調(diào)試階段完全夠用等確認(rèn)鏈路沒問題了再換大模型。另外可以在循環(huán)里加一個(gè) sleep避免一股腦發(fā)請求。5.4 回答不對檢索內(nèi)容驢頭不對馬嘴遇到最多的是這個(gè)問題代碼跑通了回答也生成了但內(nèi)容完全不在點(diǎn)上。第一步先看 source_documents。把命中的幾個(gè)文本塊打印出來看看模型依據(jù)的資料是不是真的和問題相關(guān)。如果檢索出來的就不相關(guān)那是索引或檢索參數(shù)的問題調(diào)整 chunk_size、換 Embedding 模型、增加 k 值。如果檢索出來是相關(guān)的但回答還是不對那就是 Prompt 的問題檢查模板里有沒有明確告訴模型“只能依據(jù)給定資料作答”。還有一個(gè)容易被忽略的細(xì)節(jié)測試時(shí)用文檔里的原話問先確認(rèn)鏈路是通的再改成同義表達(dá)驗(yàn)證檢索器的泛化能力。很多人一上來就用自己的大白話提問檢索不到內(nèi)容就以為是系統(tǒng)壞了其實(shí)在文檔里原話是有明確答案的。5.5 LangChain版本變動導(dǎo)致的import報(bào)錯版本兼容問題我之前被惡心過。LangChain 從 0.1 到 0.3 經(jīng)歷了好幾次大重構(gòu)很多組件改了導(dǎo)入路徑。網(wǎng)上大量教程還是舊版寫法直接復(fù)制就會 ModuleNotFoundError。新版的核心路徑寫在下面RecursiveCharacterTextSplitter 從langchain_text_splitters導(dǎo)入DirectoryLoader、TextLoader、ChatTongyi、DashScopeEmbeddings、FAISS 從langchain_community子模塊導(dǎo)入RetrievalQA、PromptTemplate 從langchain導(dǎo)入解決版本問題有一個(gè)笨辦法但很有效遇到 ModuleNotFoundError優(yōu)先檢查 import 路徑是否符合上面這些新路徑而不是急著去 pip 重裝。同時(shí)建議在項(xiàng)目 requirements.txt 里把 langchain 版本范圍寫死比如langchain0.3,0.4避免哪天長版本大版本升級后舊代碼突然跑不了。6. 可直接運(yùn)行的完整代碼文章末尾6.1 項(xiàng)目目錄結(jié)構(gòu)先把目錄結(jié)構(gòu)擺出來照著建就行rag_demo/ ├── .env # 存放 DASHSCOPE_API_KEY ├── knowledge/ # 把你想讓模型學(xué)習(xí)的文檔放這里 │ └── product_manual.txt └── main.py # 本文完整代碼首次運(yùn)行 main.py 會自動在項(xiàng)目根目錄創(chuàng)建 faiss_index 文件夾用來存放向量索引。第二次再運(yùn)行如果檢測到索引已存在就不會重新構(gòu)建直接加載本地索引。6.2 main.py 完整代碼下面這段代碼就是完整可運(yùn)行的版本。你只需要做三件事把 .env 文件里的 Key 換成自己的往 knowledge 目錄里放幾個(gè) txt 文檔然后執(zhí)行python main.py。import os from dotenv import load_dotenv load_dotenv() from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain_community.embeddings import DashScopeEmbeddings from langchain_community.vectorstores import FAISS from langchain_community.chat_models import ChatTongyi from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate DASHSCOPE_API_KEY os.getenv(DASHSCOPE_API_KEY) if not DASHSCOPE_API_KEY: raise ValueError(請先在 .env 文件中配置 DASHSCOPE_API_KEY) KNOWLEDGE_DIR ./knowledge VECTOR_INDEX_DIR ./faiss_index def build_index(): 讀取知識庫文檔切分向量化保存FAISS索引。 loader DirectoryLoader( KNOWLEDGE_DIR, glob**/*.txt, loader_clsTextLoader, loader_kwargs{encoding: utf-8}, ) documents loader.load() if not documents: raise ValueError(知識庫目錄下沒有找到任何 txt 文檔請先往 knowledge 目錄放文件。) text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, ) docs text_splitter.split_documents(documents) embeddings DashScopeEmbeddings(modeltext-embedding-v3) vectorstore FAISS.from_documents(docs, embeddings) vectorstore.save_local(VECTOR_INDEX_DIR) print(f索引構(gòu)建完成共切分 {len(docs)} 個(gè)文本塊已保存到 {VECTOR_INDEX_DIR}) def load_index(): 加載本地FAISS索引。 embeddings DashScopeEmbeddings(modeltext-embedding-v3) vectorstore FAISS.load_local( VECTOR_INDEX_DIR, embeddings, allow_dangerous_deserializationTrue, ) return vectorstore def create_qa_chain(): 構(gòu)建檢索問答鏈。 if not os.path.exists(VECTOR_INDEX_DIR): build_index() vectorstore load_index() retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 4}, ) llm ChatTongyi( modelqwen-plus, temperature0.1, ) prompt_template 請基于以下給定的資料用簡潔、專業(yè)、準(zhǔn)確的中文回答用戶的問題。 如果給定的資料中沒有相關(guān)內(nèi)容請直接回答根據(jù)現(xiàn)有知識無法回答該問題。禁止編造或臆測。 給定資料 {context} 用戶問題 {question} 你的回答 QA_CHAIN_PROMPT PromptTemplate( templateprompt_template, input_variables[context, question], ) qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, retrieverretriever, return_source_documentsTrue, chain_type_kwargs{prompt: QA_CHAIN_PROMPT}, ) return qa_chain def main(): qa_chain create_qa_chain() while True: question input(\n請輸入問題輸入 exit 或 quit 退出).strip() if question.lower() in (exit, quit): break if not question: continue result qa_chain.invoke({query: question}) print(\n回答) print(result[result]) print(\n參考來源) for i, doc in enumerate(result[source_documents], 1): source doc.metadata.get(source, 未知來源) content doc.page_content[:120].replace(\n, ) print(f{i}. {source} | {content}) print(已退出。) if __name__ __main__: main()6.3 運(yùn)行驗(yàn)證與后續(xù)優(yōu)化按順序執(zhí)行在項(xiàng)目根目錄創(chuàng)建 .env 文件寫入DASHSCOPE_API_KEY你的key。在 knowledge 目錄下放幾個(gè) txt 文檔比如產(chǎn)品說明、FAQ、操作手冊。運(yùn)行python main.py。在交互提示符后輸入問題比如“備份策略是什么”回車就能看到回答和來源。輸入 exit 退出程序。第一次運(yùn)行會先構(gòu)建索引等你第二次輸入問題時(shí)檢索和回答就已經(jīng)是實(shí)時(shí)完成的。我實(shí)際測試下來從索引構(gòu)建到拿到第一個(gè)回答整個(gè)過程通常不超過一分鐘而且大部分時(shí)間是花在文檔切分和向量化上。這個(gè)版本跑通之后還有幾個(gè)后續(xù)值得做的方向如果想支持多輪對話可以在鏈路里加一個(gè)記憶模塊把歷史對話一并傳給模型如果知識庫文檔經(jīng)常更新可以在現(xiàn)有索引上調(diào)用 add_documents 增量添加而不是每次全量重建如果想擺脫在線 API 的成本也可以把 ChatTongyi 換成本地部署的開源模型LangChain 的接口層基本不用動。這套代碼我實(shí)際跑過很多輪第一次跑通時(shí)最大的感受是RAG 真的沒有想象中復(fù)雜真正的復(fù)雜度全藏在細(xì)節(jié)里——切分多大、命中幾個(gè)塊、Prompt 怎么寫、Key 怎么配。你把這篇文章的代碼復(fù)制下來換成自己的文檔跑一圈很多“教程里為什么這么寫”的問題就自然懂了。以后要做生產(chǎn)級系統(tǒng)再把權(quán)限、日志、效果評估這些補(bǔ)上但第一步永遠(yuǎn)是把鏈路跑通。