:從零搭建知識庫,解決檢索不準與答案編造)
簡介這份源碼面向希望深入掌握大模型檢索增強生成RAG技術的Python開發(fā)者與算法學習者提供一套可運行的最佳實踐工程范例幫助理解檢索與生成如何協(xié)同提升文本處理能力適用于搜索引擎、智能問答、自動文稿撰寫等場景。資源包共22個文件、約527KB以7個XML配置文件和5個Python源碼文件為核心前者負責環(huán)境與參數(shù)配置后者承載檢索、查詢、提示詞等算法邏輯另含Markdown與文本說明、Git忽略配置、PNG示意圖、IDEA工程文件及開源許可結構完整、便于二次開發(fā)。目前已有946人學習下載。讀者可從中獲得清晰的目錄組織、模塊劃分思路與RAG實現(xiàn)骨架對照源碼快速搭建實驗環(huán)境理解配置與代碼的配合方式并借助文檔與圖示降低上手門檻適合作為進階學習與項目落地的參考模板。1. 從一份 Python RAG 源碼說起為什么你搭的知識庫總在“胡說八道”你大概率遇到過這種場景把公司幾十份 PDF 丟進一個開源 RAG 項目問它“報銷標準是多少”它答得頭頭是道數(shù)字卻是編的。翻回原文一查壓根沒這句話。這不是模型笨而是檢索環(huán)節(jié)把不相關的段落塞進了上下文大模型只能順著“喂”進來的錯誤材料往下編?;?Python 的大模型 RAG 檢索增強生成本質就是給大模型外掛一個可查證的知識庫先把文檔切塊、向量化、存進向量庫提問時先檢索出最相關的幾段再連同問題一起交給大模型生成答案。它解決的是大模型“不知道你私有數(shù)據(jù)”和“愛編造”這兩個硬傷適合手里有文檔、想快速做出可問答知識庫的 Python 開發(fā)者。這一章先把 RAG 的骨架立住后面幾章帶你從零跑通一套能落地的源碼結構把檢索命中率和答案可信度真正調上來。2. RAG 源碼的四個核心模塊切塊、向量化、檢索、生成一套能用的 RAG 源碼拆開看就是四件事文檔怎么切、切完怎么變成向量、提問時怎么找回最相關的塊、找回來怎么喂給大模型。很多人一上來就抄 LangChain 的鏈式調用跑通了卻不知道哪一步在拖后腿。我一般先把這四個模塊單獨拎出來每個都能獨立替換和調試出問題才知道該改哪。2.1 文檔切塊chunk_size 和 overlap 怎么定切塊是 RAG 里最容易被忽視、又最影響效果的一步。切太大一個塊里混了好幾個主題檢索時噪聲大切太小一句話被攔腰截斷語義不完整。常見做法是按字符數(shù)切配合重疊區(qū)防止邊界信息丟失。下面是一個不依賴重型框架的最小切塊實現(xiàn)def split_text(text, chunk_size500, overlap80): # chunk_size: 每塊目標字符數(shù)中文按字符算 # overlap: 相鄰塊重疊字符數(shù)防止句子被切斷后語義丟失 chunks [] start 0 while start len(text): end start chunk_size chunk text[start:end] chunks.append(chunk) # 下一塊起點回退 overlap 個字符形成重疊 start end - overlap if start 0 or end len(text): break return chunks邏輯說明循環(huán)每次取chunk_size個字符下一塊起點回退overlap保證跨塊的句子在兩塊里都出現(xiàn)。參數(shù)上中文技術文檔我一般用chunk_size400~600、overlap50~100如果是法律、醫(yī)療這類長句密集的文本塊可以放大到 800overlap 提到 150。判斷切得好不好有個土辦法隨機抽幾個塊讀一遍如果每塊都能獨立看懂在講什么就合格了。2.2 向量化與向量庫選型本地小模型還是調 API切完塊要轉成向量才能做語義檢索。這里有個選型分叉調云端 embedding API效果好但按量計費、數(shù)據(jù)出本地用本地開源 embedding 模型免費、數(shù)據(jù)不出門但需要一點顯存或 CPU 算力。個人知識庫和內部文檔我傾向本地模型配合 FAISS 這種輕量向量庫幾十萬塊也能扛。from sentence_transformers import SentenceTransformer import faiss import numpy as np # 本地 embedding 模型首次運行會自動下載權重 model SentenceTransformer(BAAI/bge-small-zh-v1.5) chunks [第一段文本, 第二段文本] # 實際來自切塊結果 # 生成向量并歸一化歸一化后內積等價于余弦相似度 emb model.encode(chunks, normalize_embeddingsTrue) dim emb.shape[1] # 用內積索引配合歸一化向量做余弦檢索 index faiss.IndexFlatIP(dim) index.add(np.array(emb, dtypefloat32)) # 檢索示例 query_vec model.encode([報銷標準是多少], normalize_embeddingsTrue) scores, ids index.search(np.array(query_vec, dtypefloat32), top_k3)邏輯說明normalize_embeddingsTrue把向量歸一化這樣 FAISS 的內積索引IndexFlatIP算出來就是余弦相似度省去額外轉換。top_k是召回條數(shù)一般先取 3~5后面再重排。參數(shù)上bge-small-zh-v1.5維度 512速度快、顯存占用低適合起步追求精度可換bge-large-zh但檢索延遲會上去。注意向量庫和 embedding 模型必須配套換模型就得重建索引否則向量空間對不上檢索結果全是亂的。2.3 檢索策略top_k、閾值和重排檢索不是把 top_k 結果一股腦塞給大模型就完事。召回太多噪聲進上下文模型容易被帶偏召回太少可能漏掉關鍵信息。我的做法是兩段式先用向量檢索召回 10~20 條再用一個重排模型rerank精排取前 3~5 條。沒有重排模型時至少加一個相似度閾值過濾。def retrieve(query, model, index, chunks, top_k5, score_threshold0.35): q_vec model.encode([query], normalize_embeddingsTrue) scores, ids index.search(np.array(q_vec, dtypefloat32), top_k * 4) results [] for score, idx in zip(scores[0], ids[0]): # 低于閾值的直接丟棄避免噪聲進上下文 if score score_threshold: continue results.append({text: chunks[idx], score: float(score)}) if len(results) top_k: break return results邏輯說明先多召回top_k * 4再按閾值篩最后截斷到top_k。score_threshold是關鍵參數(shù)設太高會漏召回設太低噪聲多。經(jīng)驗值bge 系列中文模型0.35~0.45 之間比較穩(wěn)具體要拿你的問題集測。判斷閾值合不合適看兩個指標——召回率該找到的有沒有找到和精確率找到的是不是都相關兩者此消彼長取平衡點。2.4 生成環(huán)節(jié)提示詞模板與上下文拼接檢索回來的塊怎么拼進提示詞直接決定答案質量。核心原則兩條一是明確告訴模型“只根據(jù)給定材料回答材料里沒有就說不知道”二是給材料編號方便模型引用來源。下面是一個能直接用的提示詞模板PROMPT_TEMPLATE 你是一個嚴謹?shù)闹R庫助手。請只根據(jù)下面提供的材料回答問題。 如果材料中沒有相關信息直接回答“根據(jù)現(xiàn)有資料無法回答”不要編造。 材料 {context} 問題{question} 回答 def build_prompt(question, retrieved): # 給每段材料編號便于追溯來源 context \n\n.join( f[{i1}] {item[text]} for i, item in enumerate(retrieved) ) return PROMPT_TEMPLATE.format(contextcontext, questionquestion)邏輯說明模板里“只根據(jù)材料回答”和“無法回答”這兩句是防幻覺的關鍵缺了模型就會自由發(fā)揮。材料編號方便你在答案里看到引用也方便排查是哪段材料導致的錯誤。參數(shù)上context總長度要控制在大模型上下文窗口內一般留出 1/3 給問題和回答剩下給材料超長就減少召回條數(shù)或壓縮塊大小。3. 從零跑通一套 RAG 源碼環(huán)境、依賴與最小可運行流程上一章拆了模塊這一章把它們串成一條能跑的流水線。我見過太多人卡在環(huán)境上——Python 版本不對、依賴沖突、模型下載失敗。這一章按順序走每一步都給可復制的命令和代碼跑完你手里就有一個能問答的最小 RAG。3.1 環(huán)境準備Python 版本與依賴清單Python 版本建議 3.10 或 3.11太老的版本部分庫不支持太新的版本有些依賴還沒跟上。用虛擬環(huán)境隔離別往系統(tǒng) Python 里裝。# 創(chuàng)建并激活虛擬環(huán)境 python -m venv rag_env source rag_env/bin/activate # Windows 用 rag_env\Scripts\activate # 安裝核心依賴 pip install sentence-transformers faiss-cpu numpy # 如需調用大模型 API再裝對應 SDK例如 pip install openai邏輯說明sentence-transformers負責 embeddingfaiss-cpu是 CPU 版向量庫有 GPU 可換faiss-gpu。依賴裝完先跑一句python -c import faiss, sentence_transformers驗證沒報錯再往下。注意faiss-cpu和faiss-gpu不能同時裝沖突了先pip uninstall干凈再裝。3.2 文檔加載與切塊把 PDF 和 Markdown 變成塊真實文檔多是 PDF、Word、Markdown 混著來。PDF 提取文本用pypdfMarkdown 直接讀。提取完統(tǒng)一走上一章的切塊函數(shù)。from pypdf import PdfReader def load_pdf(path): reader PdfReader(path) text for page in reader.pages: # extract_text 對掃描版 PDF 無效需先做 OCR text page.extract_text() or return text def load_markdown(path): with open(path, r, encodingutf-8) as f: return f.read() # 統(tǒng)一入口 def load_document(path): if path.endswith(.pdf): return load_pdf(path) elif path.endswith((.md, .txt)): return load_markdown(path) raise ValueError(f不支持的格式: {path})邏輯說明extract_text()對純文本 PDF 有效掃描件返回空字符串這種情況得先上 OCR否則后面全是空塊。加載完接切塊函數(shù)把長文本切成塊列表。參數(shù)上PDF 提取常帶多余換行和頁眉頁腳切塊前可以用正則清一遍減少噪聲。3.3 建索引與持久化一次構建多次查詢每次提問都重新算向量太浪費建好索引要存盤。FAISS 支持直接寫文件下次啟動讀回來即可。import faiss import numpy as np import pickle def build_and_save(chunks, model, index_pathindex.faiss, meta_pathmeta.pkl): emb model.encode(chunks, normalize_embeddingsTrue) index faiss.IndexFlatIP(emb.shape[1]) index.add(np.array(emb, dtypefloat32)) faiss.write_index(index, index_path) # 塊文本和索引分開存靠順序對應 with open(meta_path, wb) as f: pickle.dump(chunks, f) def load_index(index_pathindex.faiss, meta_pathmeta.pkl): index faiss.read_index(index_path) with open(meta_path, rb) as f: chunks pickle.load(f) return index, chunks邏輯說明向量存進 FAISS 索引文件原始塊文本用 pickle 單獨存兩者靠添加順序一一對應所以重建索引時塊順序不能變。參數(shù)上IndexFlatIP是精確檢索數(shù)據(jù)量到百萬級可以考慮IndexIVFFlat做近似檢索換速度但需要額外訓練索引。注意索引文件和元數(shù)據(jù)文件要一起備份丟一個就對不上。3.4 串起問答鏈路一個可運行的 main 函數(shù)把加載、切塊、建索引、檢索、生成串起來就是一個完整的最小 RAG。def main(): model SentenceTransformer(BAAI/bge-small-zh-v1.5) text load_document(docs/manual.pdf) chunks split_text(text, chunk_size500, overlap80) build_and_save(chunks, model) index, chunks load_index() while True: question input(提問q 退出) if question q: break retrieved retrieve(question, model, index, chunks) if not retrieved: print(未檢索到相關內容) continue prompt build_prompt(question, retrieved) # 這里接你的大模型調用把 prompt 發(fā)出去拿回答 print(prompt) # 先打印看拼出來的提示詞對不對 if __name__ __main__: main()邏輯說明先建索引再進問答循環(huán)retrieve返回空說明閾值卡太嚴或知識庫里真沒有直接提示用戶而不是硬答。調試階段先把拼好的 prompt 打印出來確認材料拼對了再接大模型能省很多排查時間。參數(shù)上chunk_size、overlap、score_threshold三個值建議做成配置項方便不同文檔集切換。4. 檢索質量調優(yōu)命中率上不去的四個真實原因RAG 跑通容易答得準難。檢索命中率rag hit rate是核心指標——用戶問的問題正確答案所在的那塊有沒有被召回。命中率上不去后面生成再強也白搭。這一章講四個我踩過的真實原因每個都能對應到具體參數(shù)。4.1 切塊把答案切碎了跨塊信息丟失現(xiàn)象用戶問“第三章提到的三個條件是什么”檢索回來的塊每個都只提到一個條件模型只能答出一個。原因答案本身跨了多個塊而檢索只按單塊相似度排序跨塊信息天然吃虧。解決一是加大 overlap讓相鄰塊共享更多內容二是對列表、步驟類內容切塊時按結構切而不是按字符數(shù)切保證一個邏輯單元在一塊里。我一般會在切塊前先按標題層級分段再對每段做字符切塊效果比純字符切好不少。4.2 查詢和文檔用詞不一致語義鴻溝現(xiàn)象文檔里寫“差旅費報銷標準”用戶問“出差能報多少錢”檢索不到。原因字面不重合向量相似度也不夠高。解決一是換更強的 embedding 模型中文場景 bge 系列比通用多語言模型好二是加查詢改寫讓大模型先把用戶口語化問題改寫成幾個檢索友好的查詢再分別檢索合并結果。查詢改寫這一步對命中率提升明顯代價是多一次大模型調用。4.3 top_k 和閾值設錯召回不足或噪聲過多現(xiàn)象要么該找到的沒找到要么找回來一堆不相關的。原因top_k太小漏召回score_threshold太高誤殺太低放噪聲進來。解決先關掉閾值把 top_k 開到 20人工看召回結果里正確答案排第幾這個排名就是你的上限再逐步調閾值觀察命中率和噪聲的平衡點。別拍腦袋定閾值一定要拿真實問題集測。4.4 向量庫和模型不匹配換了模型沒重建索引現(xiàn)象換了 embedding 模型后檢索結果全亂相似度普遍偏低。原因舊索引是用舊模型算的向量新查詢用新模型算兩個向量空間對不上。解決換 embedding 模型必須重建索引沒有例外。我一般把模型名寫進索引元數(shù)據(jù)加載時校驗不一致就報錯提示重建避免這種玄學問題浪費半天。5. 避坑與排查RAG 上線前必須過的五道坎前面講的是怎么調好這一章講怎么不翻車。下面五條都是我在真實項目里踩過的每條按現(xiàn)象、原因、解決寫照著排查能省不少時間。5.1 答案編造材料里沒有卻答得煞有介事現(xiàn)象問一個知識庫里根本沒有的問題模型照樣給出一段像模像樣的答案。原因提示詞沒約束“無法回答”的行為或者檢索閾值太低把不相關材料喂了進去。解決提示詞里明確寫“材料中沒有就回答無法回答”同時提高score_threshold檢索為空時直接返回固定話術不調大模型。5.2 中文亂碼PDF 提取出來全是問號現(xiàn)象PDF 加載后文本是亂碼或空白。原因PDF 用了非標準字體編碼或本身是掃描件。解決先判斷是文本型還是掃描型掃描型必須走 OCR文本型亂碼可換pdfplumber等庫重試。加載環(huán)節(jié)加一個校驗提取文本長度異常就報警別讓空塊進索引。5.3 檢索延遲高每次提問等好幾秒現(xiàn)象問答響應慢用戶等不及。原因embedding 模型太大、向量庫沒建索引、或每次都在重算文檔向量。解決文檔向量只算一次并持久化查詢向量用輕量模型數(shù)據(jù)量大時把IndexFlatIP換成 IVF 類近似索引。延遲和精度要權衡先測出瓶頸在哪一步再優(yōu)化。5.4 上下文超長材料太多把窗口撐爆現(xiàn)象報錯提示超出模型上下文長度或回答被截斷。原因召回條數(shù)太多、塊太大拼起來超過窗口。解決控制召回條數(shù)對材料做去重和壓縮必要時只保留與問題最相關的句子。我一般按“窗口的 1/3 給材料”來倒推能放幾條超了就減。5.5 更新知識庫后答案還是舊的現(xiàn)象文檔改了問答還是老答案。原因索引沒重建或者緩存沒清。解決文檔變更后觸發(fā)重建索引流程把模型名、文檔版本寫進元數(shù)據(jù)加載時校驗版本。別指望向量庫自動感知文檔變化它只認你喂進去的向量。6. 進階用重排和查詢改寫把命中率再提一檔基礎版跑通后想再往上提命中率最劃算的兩招是重排和查詢改寫。重排是在向量召回之后加一道精排用交叉編碼器cross-encoder對“問題-塊”逐對打分精度比向量相似度高代價是慢。查詢改寫是讓大模型把用戶問題擴寫成多個檢索查詢覆蓋不同表述再合并召回結果。這兩招疊加命中率通常能比基礎版明顯提升但都會增加延遲和調用成本要不要上取決于你的場景對準確率的容忍度。一個實用的組合流程是向量召回 20 條 → 重排取前 5 條 → 拼提示詞生成。重排模型可以用bge-reranker系列和 embedding 模型配套。查詢改寫則放在檢索前用一次大模型調用生成 2~3 個變體查詢分別檢索后按相似度合并去重。下面是一個合并去重的小工具def merge_results(result_lists, top_k5): # 多個查詢的召回結果合并按塊文本去重保留最高分 best {} for results in result_lists: for item in results: key item[text] if key not in best or item[score] best[key][score]: best[key] item ranked sorted(best.values(), keylambda x: x[score], reverseTrue) return ranked[:top_k]邏輯說明用塊文本做去重鍵同一塊被多個查詢召回時保留最高分最后統(tǒng)一排序截斷。參數(shù)上top_k是最終進上下文的條數(shù)別設太大。這套流程的驗證方法是準備 30~50 個真實問題標注正確答案所在塊分別測基礎版和進階版的命中率用數(shù)據(jù)決定值不值得上重排。我自己做 RAG 最大的教訓是別一上來就堆框架和花哨功能先把切塊、閾值、提示詞這三樣調扎實命中率和可信度就贏過一大半項目。每次改參數(shù)都留個記錄不然調著調著就忘了哪版最好。希望幫到你。本文還有配套的精品資源點擊獲取