LangChain 實戰專案:RAG 問答系統
全文索引 → 嵌入 → 查詢
簡介
在資訊爆炸的時代,企業與開發者常常需要從大量文件、手冊、客服紀錄中快速找出關鍵答案。傳統的關鍵字搜尋雖然方便,但無法理解語意,對於「相似」但文字不完全相同的問題往往無法正確回應。Retrieval‑Augmented Generation (RAG) 透過結合向量搜尋與大型語言模型 (LLM),讓系統在「找」與「答」兩個階段都具備語意感知能力,從而提供更精準、更自然的問答體驗。
本篇文章以 LangChain 為開發框架,示範如何從 全文索引、向量嵌入、查詢 三個步驟,完整建置一個 RAG 問答系統。內容涵蓋核心概念、實作範例、常見陷阱與最佳實踐,適合剛入門的初學者,也能為有一定基礎的開發者提供可直接上線的參考範本。
核心概念
1. 為什麼需要全文索引?
全文索引 (Full‑Text Index) 是將原始文件切割成可搜尋的單元(段落、句子、段落等),並為每個單元建立唯一的 ID。這一步驟的目的有兩個:
- 降低搜尋範圍:向量資料庫一次只能處理有限筆記錄,將文件拆成小片段可以減少每次向量搜尋的成本。
- 提升語意對應:LLM 在生成答案時,只需要參考最相關的幾段文字,而不是整篇文件,避免資訊過載。
1.1 建立文件切片
以下範例使用 LangChainJS(Node.js)將 PDF 轉成文字,並以段落為單位切片:
// src/indexDocuments.js
import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf";
import { RecursiveCharacterTextSplitter } from "@langchain/text_splitters";
export async function loadAndSplit(pdfPath) {
// 1. 讀取 PDF 並轉成 Document 陣列
const loader = new PDFLoader(pdfPath);
const rawDocs = await loader.load(); // 每頁為一個 Document
// 2. 以段落為單位切割(每個段落最多 1000 個字元,重疊 200)
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
separators: ["\n\n", "\n", " "],
});
const splitDocs = await splitter.splitDocuments(rawDocs);
return splitDocs; // 回傳切好的 Document 陣列
}
註解
PDFLoader只是一個範例,LangChain 支援多種檔案類型(txt、docx、html 等)。chunkOverlap可以防止關鍵資訊被切斷,提升向量相似度的穩定性。
2. 向量嵌入:把文字變成機器可比較的數字
向量嵌入 (Embedding) 是把自然語言映射到高維向量空間的過程。相似的句子在向量空間中距離會較近,這正是向量搜尋的核心。LangChain 內建多種嵌入模型,例如 OpenAI 的 text-embedding-ada-002、HuggingFace 的 sentence‑transformers。
2.1 建立嵌入模型與向量資料庫
以下示範使用 OpenAI 的嵌入模型,並把向量存入 Pinecone(雲端向量資料庫):
// src/createVectorStore.js
import { OpenAIEmbeddings } from "@langchain/openai";
import { PineconeStore } from "@langchain/community/vectorstores/pinecone";
import { PineconeClient } from "@pinecone-database/pinecone";
export async function buildVectorStore(docs) {
// 1. 初始化 OpenAI 嵌入模型
const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
});
// 2. 初始化 Pinecone client
const pinecone = new PineconeClient();
await pinecone.init({
environment: process.env.PINECONE_ENV,
apiKey: process.env.PINECONE_API_KEY,
});
const index = pinecone.Index(process.env.PINECONE_INDEX);
// 3. 把文件切片轉成向量並寫入 Pinecone
const vectorStore = await PineconeStore.fromDocuments(docs, embeddings, {
pineconeIndex: index,
namespace: "rag-demo",
// 可自行調整每筆資料的 metadata(例如 source、page 等)
metadataFields: ["source", "page"],
});
return vectorStore;
}
重點
- metadata 可以幫助後續追蹤答案來源,提升可解釋性。
- 若不想使用雲端服務,也可以選擇 ChromaDB、Weaviate、FAISS 等本機向量庫。
3. 查詢:從向量庫取回相關段落,再交給 LLM 產生答案
RAG 的查詢流程大致如下:
- 使用者輸入問題 → 產生問題的向量。
- 向量相似度搜尋 → 取得 Top‑k(例如 4)最相關的段落。
- 把段落與問題一起送給 LLM → LLM 以「檢索到的文件」作為上下文生成答案。
3.1 撰寫查詢鏈 (Chain)
LangChain 提供 RetrievalQAChain 讓上述流程一鍵完成。以下以 OpenAI GPT‑3.5‑Turbo 為例:
// src/queryChain.js
import { OpenAIChat } from "@langchain/openai";
import { RetrievalQAChain } from "@langchain/chains";
import { PromptTemplate } from "@langchain/prompts";
export async function askQuestion(vectorStore, question) {
// 1. 建立 LLM
const llm = new OpenAIChat({
openAIApiKey: process.env.OPENAI_API_KEY,
modelName: "gpt-3.5-turbo",
temperature: 0, // 為了答案可重現,設為 0
});
// 2. 自訂 Prompt(可根據需求調整)
const prompt = PromptTemplate.fromTemplate(`
你是一位文件助理,根據以下檔案段落回答使用者的問題。
若找不到相關資訊,請說明「我找不到答案」。
文件段落:
{context}
問題:{question}
`);
// 3. 建立 RetrievalQAChain
const chain = RetrievalQAChain.fromLLM(llm, vectorStore.asRetriever(4), {
prompt,
returnSourceDocuments: true, // 回傳來源段落,方便除錯與說明
});
// 4. 執行查詢
const result = await chain.invoke({ question });
return result; // { answer: "...", sourceDocuments: [...] }
}
說明
asRetriever(4)表示每次搜尋回傳前 4 筆最相關段落。returnSourceDocuments讓最終回應中可以附上「來源」資訊,提升系統透明度。
常見陷阱與最佳實踐
| 陷阱 | 為何會發生 | 解決方案 |
|---|---|---|
| 向量維度不一致 | 使用不同嵌入模型產生的向量維度不同,導致向量庫寫入失敗。 | 統一使用同一套嵌入模型;若需切換,務必重新建置向量庫。 |
| Chunk 太大/太小 | 太大會降低相似度精度,太小會產生過多碎片,增加搜尋成本。 | 依文件類型調整 chunkSize(約 500‑1500 token 為佳),並使用 chunkOverlap 防止關鍵資訊被切斷。 |
| LLM 輸出不受控制 | 溫度過高或 Prompt 不夠具體,會導致答案漂移或產生「幻覺」資訊。 | 設定 temperature: 0、使用指令式 Prompt,並明確要求「若找不到資訊請說明」;必要時加入 檢查機制(例如正則表達式過濾)。 |
| 向量庫未設索引 | 大規模資料未建立適當的索引(IVF、HNSW),搜尋速度會變慢。 | 依向量庫文件設定最佳化索引類型,或在本機使用 FAISS 時自行建立 IndexFlatIP、IndexIVFFlat 等。 |
| 忘記加上 metadata | 後續無法追蹤答案來源,使用者對系統失去信任。 | 在建立向量時一定要加入 source、page、timestamp 等欄位,並在回應中顯示。 |
最佳實踐小結:
- 前處理:統一文字編碼、去除雜訊(HTML 標籤、特殊符號)。
- Chunking:根據語意切片,保持段落完整性。
- Embedding:選擇成本與效能平衡的模型(OpenAI
text-embedding-ada-002性價比高)。 - 向量庫:根據預期查詢量選擇合適的雲端或本機方案,並設定好索引。
- Prompt Engineering:使用「指令式」Prompt,明確告知 LLM 只可使用檢索到的資訊。
- 結果驗證:加入簡易的後處理檢查,避免 LLM 產生未根據檔案的答案。
實際應用場景
| 場景 | 需求 | RAG 解決方案 |
|---|---|---|
| 企業內部知識庫 | 員工查詢公司政策、技術文件 | 以 PDF、Markdown 建立全文索引 → 嵌入 → 內部部署的 LLM(如 Llama‑2)提供即時答案。 |
| 客服自助系統 | 客戶常見問題 (FAQ) 多變且需即時更新 | 每次新增 FAQ 只要重新跑一次 loadAndSplit → buildVectorStore,系統即時反映。 |
| 法律條文檢索 | 律師需要在大量條文中找到相似判例 | 高維嵌入 + 小 chunk (句子級) → 精準相似度搜尋 → LLM 結合條文生成法律意見。 |
| 教育平台 | 學生提問課程教材的細節 | 把教材切成章節 → 嵌入 → 讓 ChatGPT 以教材內容作為上下文回答。 |
| 醫療資訊查詢 | 醫師查找最新研究摘要 | 把 PubMed 論文摘要向量化,使用 RAG 產生簡潔的研究結論。 |
提示:在高度敏感或合規需求的領域(如醫療、金融),務必在 Prompt 中加入「僅根據檢索結果回答」的限制,並在系統層面加上審核機制。
總結
本文以 LangChain 為核心,完整展示了 全文索引 → 向量嵌入 → 查詢 三大步驟如何組合成一套可在實務中直接使用的 RAG 問答系統。關鍵要點包括:
- 文件切片:使用
RecursiveCharacterTextSplitter產生語意完整的段落。 - 向量化:選擇合適的嵌入模型並將向量寫入 Pinecone(或其他向量庫)。
- 檢索 + 生成:透過
RetrievalQAChain把相似段落與 LLM 結合,產出可靠答案。 - 最佳實踐:控制 chunk 大小、設定適當的 temperature、加入 metadata、優化向量索引。
掌握以上流程後,你即可快速為 企業知識庫、客服系統、教育平台 等多種應用打造具備語意搜尋與自動生成能力的智慧問答服務。未來隨著模型與向量資料庫的持續進化,RAG 方案將成為構建「人機共讀」的關鍵技術,值得每位開發者深入探索與實踐。祝你在 LangChain 的旅程中玩得開心、寫出好程式!