本文 AI 產出,尚未審核

LangChain 實戰專案:RAG 問答系統

全文索引 → 嵌入 → 查詢


簡介

在資訊爆炸的時代,企業與開發者常常需要從大量文件、手冊、客服紀錄中快速找出關鍵答案。傳統的關鍵字搜尋雖然方便,但無法理解語意,對於「相似」但文字不完全相同的問題往往無法正確回應。Retrieval‑Augmented Generation (RAG) 透過結合向量搜尋與大型語言模型 (LLM),讓系統在「找」與「答」兩個階段都具備語意感知能力,從而提供更精準、更自然的問答體驗。

本篇文章以 LangChain 為開發框架,示範如何從 全文索引向量嵌入查詢 三個步驟,完整建置一個 RAG 問答系統。內容涵蓋核心概念、實作範例、常見陷阱與最佳實踐,適合剛入門的初學者,也能為有一定基礎的開發者提供可直接上線的參考範本。


核心概念

1. 為什麼需要全文索引?

全文索引 (Full‑Text Index) 是將原始文件切割成可搜尋的單元(段落、句子、段落等),並為每個單元建立唯一的 ID。這一步驟的目的有兩個:

  1. 降低搜尋範圍:向量資料庫一次只能處理有限筆記錄,將文件拆成小片段可以減少每次向量搜尋的成本。
  2. 提升語意對應: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 可以幫助後續追蹤答案來源,提升可解釋性。
  • 若不想使用雲端服務,也可以選擇 ChromaDBWeaviateFAISS 等本機向量庫。

3. 查詢:從向量庫取回相關段落,再交給 LLM 產生答案

RAG 的查詢流程大致如下:

  1. 使用者輸入問題 → 產生問題的向量。
  2. 向量相似度搜尋 → 取得 Top‑k(例如 4)最相關的段落。
  3. 把段落與問題一起送給 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 時自行建立 IndexFlatIPIndexIVFFlat 等。
忘記加上 metadata 後續無法追蹤答案來源,使用者對系統失去信任。 在建立向量時一定要加入 sourcepagetimestamp 等欄位,並在回應中顯示。

最佳實踐小結

  1. 前處理:統一文字編碼、去除雜訊(HTML 標籤、特殊符號)。
  2. Chunking:根據語意切片,保持段落完整性。
  3. Embedding:選擇成本與效能平衡的模型(OpenAI text-embedding-ada-002 性價比高)。
  4. 向量庫:根據預期查詢量選擇合適的雲端或本機方案,並設定好索引。
  5. Prompt Engineering:使用「指令式」Prompt,明確告知 LLM 只可使用檢索到的資訊。
  6. 結果驗證:加入簡易的後處理檢查,避免 LLM 產生未根據檔案的答案。

實際應用場景

場景 需求 RAG 解決方案
企業內部知識庫 員工查詢公司政策、技術文件 以 PDF、Markdown 建立全文索引 → 嵌入 → 內部部署的 LLM(如 Llama‑2)提供即時答案。
客服自助系統 客戶常見問題 (FAQ) 多變且需即時更新 每次新增 FAQ 只要重新跑一次 loadAndSplitbuildVectorStore,系統即時反映。
法律條文檢索 律師需要在大量條文中找到相似判例 高維嵌入 + 小 chunk (句子級) → 精準相似度搜尋 → LLM 結合條文生成法律意見。
教育平台 學生提問課程教材的細節 把教材切成章節 → 嵌入 → 讓 ChatGPT 以教材內容作為上下文回答。
醫療資訊查詢 醫師查找最新研究摘要 把 PubMed 論文摘要向量化,使用 RAG 產生簡潔的研究結論。

提示:在高度敏感或合規需求的領域(如醫療、金融),務必在 Prompt 中加入「僅根據檢索結果回答」的限制,並在系統層面加上審核機制


總結

本文以 LangChain 為核心,完整展示了 全文索引 → 向量嵌入 → 查詢 三大步驟如何組合成一套可在實務中直接使用的 RAG 問答系統。關鍵要點包括:

  • 文件切片:使用 RecursiveCharacterTextSplitter 產生語意完整的段落。
  • 向量化:選擇合適的嵌入模型並將向量寫入 Pinecone(或其他向量庫)。
  • 檢索 + 生成:透過 RetrievalQAChain 把相似段落與 LLM 結合,產出可靠答案。
  • 最佳實踐:控制 chunk 大小、設定適當的 temperature、加入 metadata、優化向量索引。

掌握以上流程後,你即可快速為 企業知識庫、客服系統、教育平台 等多種應用打造具備語意搜尋與自動生成能力的智慧問答服務。未來隨著模型與向量資料庫的持續進化,RAG 方案將成為構建「人機共讀」的關鍵技術,值得每位開發者深入探索與實踐。祝你在 LangChain 的旅程中玩得開心、寫出好程式!