還在為大模型“胡言亂語”頭疼?不用微調,不用燒顯卡,一個被無數大廠驗證過的技術方案——RAG,就能讓大模型瞬間學會你公司的私有知識。今天,我們用最通俗的語言,把這項技術的每一個細節(jié)都掰開揉碎講清楚。
01 大模型的兩個“致命傷”
先問你一個問題:如果讓你在完全不查資料的情況下,回答“2024年諾貝爾物理學獎頒給了誰?”你能答上來嗎?
大概率不能——因為你的“知識”停留在幾年前,這就是知識滯后。
大 模型 也一樣。它雖然在海量數據上訓練了很久,但訓練一次動輒幾個月,花費幾千萬甚至上億美金。訓練完成之后,它的知識就“定格”了。你問它2024年發(fā)生的事情,它要么說不知道,要么就按照以前的規(guī)律瞎編。
另一個問題是知識缺失。你家公司內部的規(guī)章制度、產品文檔、客戶案例,大模型根本沒見過。你問它“我們公司年假怎么休”,它只能跟你道歉說“我無法回答”。
最要命的是第三個問題:幻覺。
幻覺就是大模型“一本正經地胡說八道”。比如你問“哈利波特第一次騎自行車是什么時候?”它可能很自信地回答“1997年,在女貞路4號。”——聽起來像模像樣,但全是編的。書里根本沒這回事。
為什么會這樣?因為大模型本質是一個“文字接龍高手”,它不知道對錯,只知道“根據前面的文字,下一個字最可能是什么”。這就好比一個學霸,雖然能背很多書,但如果你問他書本上沒有的內容,他就會開始“自由發(fā)揮”。
有一個真實案例:某律師用大模型準備庭審材料,大模型“引用”了好幾個判例,還附上了詳細的案號、法官名字。結果對方律師一查——這些判例全是編的!這位律師因此被罰款。這不是段子,是真事。
那么,有沒有辦法讓大模型既能發(fā)揮它的語言天賦,又能保證回答有據可查?
有。這個辦法就是 RAG。
02 RAG 是什么?開卷考試了解一下
RAG 的全稱是?Retrieval-Augmented Generation,中文叫“檢索增強生成”。名字很唬人,但本質就四個字:開卷考試。
想一想,開卷考試和閉卷考試有什么區(qū)別?
-
閉卷考試:全靠記憶。大模型現在就是閉卷模式。
-
開卷考試:允許你翻書查資料。RAG 就是給大模型配一本“參考書”。
具體流程是這樣:
-
你問一個問題:“我們公司年假怎么休?”
-
RAG 系統(tǒng)先去你公司的“知識庫”(比如員工手冊、公司制度文檔)里,把相關的內容搜出來。
-
然后把搜到的內容,連同你的問題,一起交給大模型。
-
大模型看著這些“參考資料”來組織答案。
這樣一來,大模型的回答就有了事實依據,不再是空口白話。而且你隨時可以更新知識庫里的文檔,大模型的知識也就跟著更新了,不需要重新訓練。
RAG 的另一個優(yōu)點是保護隱私。你的私有數據不需要喂給大模型訓練,只用在檢索那一刻臨時調用。數據始終在你自己的數據庫里。
典型的RAG有兩個主要流程:
索引:從數據源提取數據,構建索引。
檢索生成:接受用戶查詢并從索引中檢索相關數據,然后將其傳遞給模型。。
索引階段:
-
從各種數據源加載數據
-
將文檔切分為小塊
-
對文本塊進行嵌入
-
存儲嵌入向量。

檢索生成階段:
-
根據用戶輸入,使用檢索器從存儲中檢索相關文本塊
-
大模型使用包含問題和檢索結果的提示生成回答。

接下來,我們就按這個流程,用代碼一步步搭建一個 RAG 系統(tǒng)。代碼注釋會非常詳細,哪怕你第一次接觸也能看懂。
03 第一步:加載文檔——讓程序“讀”懂你的文件
你的知識可能散落在各種格式的文件里:Word 文檔、PDF、Excel、網頁、 Markdown ……第一步要做的,就是把它們統(tǒng)統(tǒng)讀進來,變成程序能處理的數據。
LangChain 是目前最流行的 RAG 框架,它提供了非常多的“文檔加載器”。你可以把它想象成各種格式的“文件打開器”。
3.1 加載純文本文件(TXT)
# 安裝 LangChain 社區(qū)擴展包
# 在命令行執(zhí)行:pip install langchain_community
from langchain_community.document_loaders import TextLoader
# 創(chuàng)建一個加載器,告訴它文件在哪,用什么編碼(中文用 utf-8)
loader = TextLoader(
? ? file_path="公司員工手冊.txt", ? # 你的文件路徑
? ? encoding="utf-8" ? ? ? ? ? ? ? # 中文編碼
)
# 執(zhí)行加載,得到文檔對象列表
# 每個文檔對象包含兩部分:page_content(文字內容)和 metadata(元數據,比如文件名)
documents = loader.load()
# 打印看看結果
print(f"加載了 {len(documents)} 個文檔")
print(f"第一個文檔的內容前200字:{documents[0].page_content[:200]}")
print(f"元數據:{documents[0].metadata}")
注:load() 方法會一次性把整個文件讀到內存。如果文件非常大(比如幾百MB),建議用 lazy_load(),它會像“逐行讀取”一樣,一次只加載一部分,節(jié)省內存。
3.2 加載 CSV 表格
CSV 是表格文件,每一行是一條記錄。加載時,你可以指定哪一列作為“正文內容”,哪幾列作為“元數據”。
from langchain_community.document_loaders.csv_loader import CSVLoader
# 最簡單的用法:把每一行都轉成一個文檔
loader = CSVLoader(file_path="客戶反饋.csv")
docs = loader.load()
# 這樣每一行會變成:page_content 包含整行的內容,metadata 里只有 source 文件名
# 進階用法:精確控制哪些列放哪里
loader = CSVLoader(
? ? file_path="客戶反饋.csv",
? ? metadata_columns=["客戶ID", "反饋時間"], ? # 這些列作為元數據(用來過濾、追溯)
? ? content_columns=["反饋內容"] ? ? ? ? ? ? ? # 這一列作為正文(用來檢索)
)
docs = loader.load()
3.3 加載 JSON 數據
JSON 是很常見的數據格式,但它的結構可能很復雜(嵌套對象、數組)。LangChain 用 jq 語法來精確提取你想要的部分。
# pip install jq
from langchain_community.document_loaders import JSONLoader
# 假設你的 JSON 長這樣:
# {
# ? "articles": [
# ? ? {"title": "RAG入門", "content": "RAG是一種...", "author": "張三"},
# ? ? {"title": "向量數據庫", "content": "Milvus是...", "author": "李四"}
# ? ]
# }
# 提取 articles 數組中的每一項,把整項作為文檔內容
loader = JSONLoader(
? ? file_path="知識庫.json",
? ? jq_schema=".articles[]", ? # jq 語法:取出 articles 數組的每個元素
? ? text_content=False ? ? ? ? ?# 保持原始 JSON 結構,不強制轉成字符串
)
docs = loader.load()
# 更精細:只提取 content 字段作為正文,把 title 和 author 合并作為元數據
loader = JSONLoader(
? ? file_path="知識庫.json",
? ? jq_schema="""
? ? ? ? .articles[] | {
? ? ? ? ? ? content: .content,
? ? ? ? ? ? metadata: {title: .title, author: .author}
? ? ? ? }
? ? """,
? ? text_content=True ? # 把構造好的對象轉成 JSON 字符串作為 page_content
)
jq 小貼士:. 代表當前節(jié)點;[] 表示遍歷數組;| 是管道,把左邊的結果傳給右邊;{key: value} 是構造新對象。學習這幾個符號,就能應付大部分場景。
3.4 加載網頁
有時候你需要爬取某個網頁的內容作為知識來源。LangChain 集成了 BeautifulSoup 來解析 HTML。
# pip install beautifulsoup4
import bs4
from langchain_community.document_loaders import WebBaseLoader
# 加載百度百科的一個頁面,并且只提取正文區(qū)域(class="J-lemma-content")
loader = WebBaseLoader(
? ? web_paths=["https://baike.baidu.com/item/RAG/12345"], ?# 可以傳多個網址
? ? bs_kwargs={
? ? ? ? "parse_only": bs4.SoupStrainer(class_="J-lemma-content") ?# 只抓這個 class 里的內容
? ? }
)
docs = loader.load()
3.5 加載 PDF——最難啃的骨頭
PDF 看起來簡單,實際上格式非常復雜。有的 PDF 是文字版(可以直接選中文字),有的是掃描版(圖片,需要 OCR 識別),還有的混合表格、圖片、雙欄排版。
如果你的 PDF 比較簡單(純文字,沒有復雜表格),可以用最簡單的 PyPDFLoader:
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("產品說明書.pdf")
docs = loader.load()
如果 PDF 有掃描圖片或復雜排版,建議用 UnstructuredPDFLoader,它支持 OCR 識別圖片中的文字,還能識別表格結構。
# pip install unstructured[local-inference]
# 還需要安裝 Poppler(PDF渲染)和 Tesseract(OCR),具體安裝方法網上搜一下
from langchain_community.document_loaders import UnstructuredPDFLoader
loader = UnstructuredPDFLoader(
? ? file_path="掃描版合同.pdf",
? ? mode="elements", ? ? ? ? ? # elements 模式會按標題、段落、表格等元素切分
? ? strategy="hi_res", ? ? ? ? # hi_res = 高精度解析(最慢但最準)
? ? infer_table_structure=True, # 嘗試解析表格,結果放在 metadata 的 text_as_html 里
? ? languages=["chi_sim"] ? ? ?# OCR 用簡體中文
)
docs = loader.load()
實戰(zhàn)經驗:如果你的項目對文檔解析質量要求很高(比如法律合同、學術論文),可以考慮付費服務如 MinerU、Mathpix 等,它們對公式、表格、多欄排版的識別效果遠超開源方案。很多大廠的 RAG 項目,一半以上的工作量都花在了文檔解析這一步。
04 第二步:文檔切分——把大塊肉切成適口小塊
文檔加載完之后,你可能得到一個很長的文檔(比如一本員工手冊,幾十頁)。你不能直接把整個手冊喂給大模型,因為:
-
大模型能接收的文本長度有限(比如 4k、8k、128k tokens)。超出的部分會被截斷。
-
即使模型能接收很長的文本,無關信息太多也會干擾它。就像讓你在整本百科全書里找一個知識點,還不如只給你相關的那一頁。
所以,我們需要把長文檔切成一個個小“塊”(Chunk),每個塊大概幾百到上千字。
4.1 用什么策略切?
最簡單的辦法是按固定字符數切,比如每 500 字切一塊。但這會把一個句子攔腰切斷。比如“我愛吃蘋果”在“我愛”后面切了,那么“吃蘋果”就失去了主語。
更好的辦法是按標點符號切:優(yōu)先在句號、問號、感嘆號處切,如果沒有,再在逗號、空格處切。這樣能保證每個塊都是完整的句子或段落。
LangChain 提供的 RecursiveCharacterTextSplitter 就是這種“遞歸切分器”。它有一個分隔符列表,按優(yōu)先級從高到低嘗試切分。
# pip install langchain-text-splitters
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 先加載一個長文檔
from langchain_community.document_loaders import TextLoader
loader = TextLoader("員工手冊.txt", encoding="utf-8")
doc = loader.load()
# 創(chuàng)建切分器
splitter = RecursiveCharacterTextSplitter(
? ? separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""],
? ? # 解釋:先找兩個換行(段落分隔),沒有就找一個換行,沒有就找句號,依次類推
? ? # 最后一個空字符串 "" 表示如果所有分隔符都找不到,就按字符硬切
? ? chunk_size=500, ? ? ?# 每個塊最多 500 個字符
? ? chunk_overlap=50, ? ?# 相鄰塊重疊 50 個字符(防止邊界信息丟失)
? ? add_start_index=True # 記錄每個塊在原文檔中的起始位置
)
chunks = splitter.split_documents(doc)
print(f"原文檔切成了 {len(chunks)} 個塊")
print(f"第一個塊的內容:{chunks[0].page_content}")
4.2 兩個關鍵參數:chunk_size 和 chunk_overlap

-
chunk_size(塊大小)
:決定了每個塊包含多少信息。
-
太?。ū热?200 字):每個塊信息太少,可能找不到足夠的上下文。
-
太大(比如 2000 字):塊內雜音多,而且浪費 Token(大模型按 Token 收費的)。
-
經驗值:對于中文,500~1000 字符是比較好的范圍。
-
-
chunk_overlap(重疊大?。?/strong>
:相鄰塊之間重復的部分。
-
為什么需要重疊?比如一個關鍵句子正好在切分邊界上,如果沒有重疊,它可能被切成兩半,分別屬于兩個塊,檢索時哪一半都不完整。有了重疊,這個句子會完整地出現在兩個塊里。
-
一般取 chunk_size 的 10%~20%。
-
你可以這樣理解:切分就像把一長串香腸切成小段,每段長度 chunk_size,但是切的時候刀口稍微退回去一點(chunk_overlap),這樣相鄰兩段會有一部分重合,保證接頭處的信息不丟失。
05 第三步:向量嵌入——給文字“貼坐標”
計算機不認識文字,但認識數字。嵌入模型的作用就是把一段文字轉換成一串數字(向量),這串數字代表了文字的“語義”。
想象一下:你把所有文檔塊放在一個巨大的多維空間里,意思相近的塊會靠得很近。比如“蘋果手機”和“iPhone”這兩個詞的向量距離很近;而“蘋果手機”和“香蕉”的距離很遠。
5.1 選擇一個好用的中文嵌入模型
目前國產的 BAAI(北京智源研究院)推出的 BGE 系列模型,對中文的支持非常好,而且是開源的。
|
模型名 |
向量維度 |
速度 |
效果 |
推薦場景 |
|
bge-small-zh |
512 |
快 |
良好 |
資源有限、快速驗證 |
|
bge-base-zh |
768 |
中等 |
很好 |
大多數場景推薦 |
|
bge-large-zh |
1024 |
慢 |
最好 |
追求極致準確率 |
|
bge-m3 |
1024 |
慢 |
最好+多語言 |
文檔含多種語言 |
向量維度:可以理解為坐標的“維度”。三維空間能區(qū)分前后左右上下,維度越高,能表達的語義細節(jié)越豐富,但計算量也越大。
5.2 用代碼生成向量
# pip install sentence-transformers langchain_huggingface
from langchain_huggingface import HuggingFaceEmbeddings
# 加載模型(第一次運行會自動下載,大概 400MB)
# 如果你已經下載到本地,可以用 os.path.expanduser("~/models/bge-base-zh-v1.5")
embed_model = HuggingFaceEmbeddings(
? ? model_name="BAAI/bge-base-zh-v1.5" ? # 也可以換成其他模型名
)
# 單個文本嵌入(用于用戶的問題)
question = "年假有多少天?"
question_vector = embed_model.embed_query(question)
print(f"問題向量長度:{len(question_vector)}") ?# 輸出 768
print(f"向量前5個值:{question_vector[:5]}") ? ?# 一堆小數
# 批量文本嵌入(用于文檔塊)
doc_texts = ["員工每年享有5天帶薪年假", "產假為98天", "婚假3天"]
doc_vectors = embed_model.embed_documents(doc_texts)
print(f"文檔塊數量:{len(doc_vectors)},每個向量長度:{len(doc_vectors[0])}")
注意:embed_query 和 embed_documents 雖然底層都是生成向量,但有些模型會對查詢和文檔做不同的處理(比如添加特殊前綴),所以不要混用。用戶的問題永遠用 embed_query,文檔塊永遠用 embed_documents。
06 第四步:向量數據庫——給向量們安個家
生成了向量之后,需要把它們存起來,并且能夠快速查找。這就是向量 數據庫 干的事。
你可以把向量數據庫想象成一個超級智能的“圖書館”:你給它一段文字,它立刻告訴你圖書館里哪幾本書的內容和這段文字最像。
6.1 選擇哪個向量數據庫?
-
Chroma
:輕量級,純 Python,幾行代碼就能跑起來,適合學習和原型驗證。
-
FAISS
:Facebook 出品,檢索速度極快,但不提供持久化存儲(重啟就丟),適合研究用途。
-
Milvus
:功能最強大,支持分布式、標量過濾、高并發(fā),適合生產環(huán)境。
我們這里用?Milvus Lite?版本,它可以直接在本地文件上運行,不需要搭建服務器,非常適合學習。

6.2 安裝和初始化
pip install pymilvus[milvus-lite]
from pymilvus import MilvusClient
# 創(chuàng)建一個客戶端,數據會保存在當前目錄下的 milvus_demo.db 文件里
client = MilvusClient(uri="./milvus_demo.db")
6.3 設計數據表結構(Schema)
在 Milvus 里,表叫 Collection。我們需要告訴它每個字段叫什么、什么類型。
from pymilvus import DataType
# 定義表結構
def create_schema():
? ? schema = MilvusClient.create_schema(
? ? ? ? auto_id=True, ? ? ? ? ? # 自動生成主鍵ID,不需要自己提供
? ? ? ? enable_dynamic_field=True ?# 允許動態(tài)添加未定義的字段
? ? )
? ? # 添加字段:id(主鍵,整數類型)
? ? schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True)
? ? # 添加字段:vector(向量,768維,浮點數)
? ? schema.add_field(field_name="vector", datatype=DataType.FLOAT_VECTOR, dim=768)
? ? # 添加字段:text(原文內容,最長1024個字符)
? ? schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=1024)
? ? # 添加字段:metadata(元數據,JSON格式,可以放任意結構)
? ? schema.add_field(field_name="metadata", datatype=DataType.JSON)
? ? return schema
# 創(chuàng)建索引(索引能加速檢索)
def create_index():
? ? index_params = MilvusClient.prepare_index_params()
? ? index_params.add_index(
? ? ? ? field_name="vector", ? ? ? ?# 對向量字段建立索引
? ? ? ? index_type="AUTOINDEX", ? ? # 自動選擇最合適的索引類型
? ? ? ? metric_type="L2" ? ? ? ? ? ?# 相似度計算方式:L2=歐氏距離(值越小越相似)
? ? )
? ? return index_params
metric_type 的三種選擇:
L2
(歐氏距離):兩點之間的直線距離,越小越相似。最直觀。
IP
(內積):向量點積,越大越相似(需要先對向量做歸一化)。
COSINE
(余弦相似度):關注方向而非長度,1表示完全相同。文本相似度通常用這個。
為了簡單,我們先用 L2。
6.4 創(chuàng)建表并插入數據
# 如果表已經存在,先刪除(方便重復運行)
collection_name = "demo_collection"
if client.has_collection(collection_name):
? ? client.drop_collection(collection_name)
# 創(chuàng)建表
client.create_collection(
? ? collection_name=collection_name,
? ? schema=create_schema(),
? ? index_params=create_index()
)
# 假設我們已經有了切分好的 chunks 和對應的向量
# chunks: List[Document],每個 Document 有 page_content 和 metadata
# embeddings: List[List[float]],每個向量與 chunks 一一對應
# 把數據組裝成 Milvus 需要的格式
data_to_insert = []
for chunk, vector in zip(chunks, embeddings):
? ? data_to_insert.append({
? ? ? ? "vector": vector, ? ? ? ? ? ? ? ? ? ? # 向量
? ? ? ? "text": chunk.page_content, ? ? ? ? ? # 原文
? ? ? ? "metadata": chunk.metadata ? ? ? ? ? ?# 元數據(如文件名、頁碼等)
? ? })
# 插入數據
insert_result = client.insert(
? ? collection_name=collection_name,
? ? data=data_to_insert
)
print(f"插入了 {insert_result['insert_count']} 條數據")
6.5 檢索:根據問題找最相似的塊

當用戶問一個問題時,我們先把問題轉成向量,然后去向量數據庫里找最相似的 k 個文檔塊。
def search_similar(query_text, embed_model, client, top_k=3):
? ? # 1. 把問題轉成向量
? ? query_vector = embed_model.embed_query(query_text)
? ? # 2. 在數據庫里搜索
? ? results = client.search(
? ? ? ? collection_name="demo_collection",
? ? ? ? data=[query_vector], ? ? ? ? ? ? ?# 要搜索的向量
? ? ? ? anns_field="vector", ? ? ? ? ? ? ?# 在哪個向量字段里搜索
? ? ? ? search_params={"metric_type": "L2"}, ?# 使用歐氏距離
? ? ? ? output_fields=["text", "metadata"], ? # 要返回哪些字段
? ? ? ? limit=top_k ? ? ? ? ? ? ? ? ? ? ? # 返回最相似的前 top_k 個
? ? )
? ? # results 的結構:[[{id, distance, entity}, ...]]
? ? # 提取出原文
? ? contexts = [hit["entity"]["text"] for hit in results[0]]
? ? return contexts
# 測試
query = "公司年假怎么計算?"
contexts = search_similar(query, embed_model, client)
for i, ctx in enumerate(contexts):
? ? print(f"結果{i+1}:\n{ctx}\n")
6.6 關于“近似最近鄰”( ANN )的通俗解釋
你可能會好奇:數據庫里可能有幾十萬條向量,怎么能在幾毫秒內找到最相似的?
如果老老實實把所有向量都算一遍(這叫 KNN,精確最近鄰),數據量一大就慢死了。
聰明的方法是近似最近鄰(ANN):事先給向量建一個“索引”,就像書的目錄。查找時,不需要翻遍整本書,而是先看目錄找到大概的章節(jié),再去那幾頁里仔細找。
Milvus 默認用的 HNSW 算法,你可以這樣理解:
-
想象你要在一個大城市里找一個人。如果你從大街上一個人一個人地問,累死。
-
更好的辦法:先找這個人可能在哪個區(qū)(上層索引),再去那個區(qū)里找街道(中層),最后到街道上找門牌號(底層)。
-
HNSW 就是建立了一個多層地圖,上層“跳”得快,下層找得準。

這個索引只需要建一次(插入數據時),之后每次檢索都享受加速。
07 第五步:生成——讓大模型看著參考資料寫答案
最后一步,把檢索到的相關文檔塊,連同用戶的問題,一起發(fā)給大模型,讓它生成答案。
7.1 構建提示詞(Prompt)
提示詞就是你對大模型說的話。一個好的提示詞要包含三部分:
-
角色設定
:你是一個專業(yè)的HR助手...
-
參考資料
:以下是從員工手冊中檢索到的內容...
-
用戶問題
:請根據參考資料回答...
from langchain_core.prompts import ChatPromptTemplate
template = ChatPromptTemplate.from_messages([
? ? ("system",?
? ? ?"你是一個專業(yè)的HR助手。請根據下面的【參考資料】回答用戶的問題。"
? ? ?"如果參考資料里沒有相關信息,請如實說‘資料中沒有提到’,不要編造答案。"
? ? ?"\n\n【參考資料】\n{context}"
? ? ),
? ? ("human", "{question}")
])
7.2 調用大模型
你可以使用 OpenAI、國產模型(如智譜、通義千問),或者本地部署的模型。這里以 OpenAI 兼容接口為例:
# pip install langchain-openai
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
? ? model="gpt-3.5-turbo", ? # 或 "gpt-4"
? ? temperature=0, ? ? ? ? ? # 溫度越低,回答越確定(不隨意發(fā)揮)
? ? api_key="你的API密鑰", ? ?# 也可以從環(huán)境變量讀取
? ? base_url="https://api.openai.com/v1" ?# 如果是其他廠商,換成對應地址
)
7.3 把檢索和生成串起來
LangChain 提供了 RunnablePassthrough 和 RunnableLambda 來構建處理鏈。
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from langchain_core.output_parsers import StrOutputParser
# 定義一個函數:檢索上下文
def retrieve_context(question):
? ? return search_similar(question, embed_model, client, top_k=3)
# 構建鏈
rag_chain = (
? ? {
? ? ? ? "question": RunnablePassthrough(), ? ? ? ? ? # 原樣傳遞問題
? ? ? ? "context": RunnableLambda(retrieve_context) ?# 調用檢索函數
? ? }
? ? | template ? ? ? ? ? ?# 填入提示詞模板
? ? | llm ? ? ? ? ? ? ? ? # 調用大模型
? ? | StrOutputParser() ? # 提取輸出文本
)
# 執(zhí)行
question = "新員工的年假是怎么計算的?"
answer = rag_chain.invoke(question)
print(answer)
7.4 流式輸出 (像 ChatGPT 那樣一個字一個字出來)
如果你想實現打字機效果,可以用 stream 方法:
for chunk in rag_chain.stream("試用期員工有年假嗎?"):
? ? print(chunk, end="", flush=True)
08 總結:RAG 的優(yōu)缺點和適用場景
優(yōu)點
-
知識實時更新
:只需要往數據庫里加新文檔,不需要重新訓練模型。
-
可溯源
:答案有據可查,可以告訴用戶“這是根據第X頁的文檔得出的”。
-
降低幻覺
:大模型有參考資料,胡說八道的概率大大降低。
-
保護隱私
:私有數據留存在你自己的數據庫里,不用交給模型廠商。
缺點
-
響應延遲
:每次問答都要做檢索+生成兩步,比直接調用大模型慢一些。
-
消耗更多 Token
:把檢索到的上下文也塞進提示詞里,Token 用量會增加。
-
檢索效果決定上限
:如果沒檢索到相關的資料,大模型再強也沒用。
什么時候該用 RAG?
??適合 RAG:
-
知識頻繁更新(公司制度、產品文檔、新聞資訊)
-
領域知識不在大模型訓練數據中(企業(yè)內部資料、私有數據庫)
-
需要答案有據可查(法律、醫(yī)療、金融)
-
不想承擔微調模型的高昂成本
??不適合 RAG:
-
任務本身不需要外部知識(比如寫一首詩、做數學計算)
-
實時性要求極高(毫秒級響應)
-
文檔量極小,且基本不變(那直接提示詞里寫死就行了)
最后的話
RAG 并不是什么高深莫測的技術,它只是巧妙地利用了“檢索”這個傳統(tǒng)技術,給大模型裝上了一雙可以翻書的“手”。很多人一開始覺得大模型很神奇,但當你理解了它的局限,再配合 RAG,你會發(fā)現:一個可控、可靠、可更新的 AI 系統(tǒng),并不需要幾十億的投入,只需要你按照這篇文章的步驟,一步步搭建起來。
如果你的團隊也想落地一個智能問答機器人,建議從最簡單的 Chroma + 本地小模型開始,跑通整個流程。等有了實際需求和數據量,再平滑遷移到 Milvus + 商業(yè)大模型。一步一個腳印,你會發(fā)現 RAG 遠比想象中簡單。