本文 AI 產出,尚未審核

LangChain 教學:Conversational RAG(對話式檢索增強生成)


簡介

在資訊爆炸的時代,單純的大型語言模型(LLM)仍然會受到「知識截止」與「資訊可信度」的限制。Retrieval‑Augmented Generation(RAG) 透過即時檢索外部文件,讓模型的回答能夠根據最新、最相關的資料產出。

當我們把 RAG 與對話流程結合,就形成 Conversational RAG,即在多輪對話中持續利用檢索結果作為上下文,提升答案的正確性與一致性。這樣的技術在客服機器人、企業內部知識庫、教育助教等場景裡,能顯著降低誤答率、提升使用者體驗。

本篇文章將以 LangChain(JavaScript 版)為例,說明如何在 Node.js 環境下快速構建 Conversational RAG,並提供實作範例、常見陷阱與最佳實踐,幫助你從零開始打造可商用的對話式檢索系統。


核心概念

1. RAG 基本流程

  1. 檢索(Retrieval):根據使用者的提問,從向量資料庫或全文搜索引擎取出最相關的文件片段。
  2. 整合(Augmentation):將檢索到的片段與對話歷史合併,形成「擴充上下文」。
  3. 生成(Generation):將擴充上下文送入 LLM,產生最終回應。

關鍵:檢索結果必須與對話歷史同步更新,才能避免「資訊斷層」或「上下文遺忘」的問題。

2. LangChain 中的主要組件

組件 功能說明
ChatOpenAI 與 OpenAI Chat API(如 gpt‑4o)互動的 LLM 包裝器。
VectorStoreRetriever 從向量資料庫(如 Pinecone、Weaviate、FAISS)取得最相似的文件。
ConversationBufferMemory 保存對話歷史,支援自動把歷史加入提示詞。
ConversationalRetrievalChain 把檢索、記憶、生成三個步驟串成一條完整的 Chain。

3. 為什麼需要 ConversationBufferMemory?

在多輪對話中,使用者可能會「省略」先前已說過的資訊,例如:

User: 請幫我找出 2023 年台北市的平均氣溫。
Assistant: (回傳結果)...
User: 那去年呢?

如果僅把「那去年呢?」當作單獨的查詢,檢索模型會失去「台北市」的前置條件。ConversationBufferMemory 會自動把前一次的完整問題與答案納入新查詢的上下文,確保檢索與生成保持一致。


程式碼範例

以下範例採用 LangChain.js(v0.1+) 與 OpenAI、Pinecone 作為向量資料庫。所有程式碼都已加上說明註解,請依序執行。

1️⃣ 初始化環境

npm install langchain openai @pinecone-database/pinecone
// loadEnv.js
require('dotenv').config();   // 讀取 .env 中的 API 金鑰

.env 範例:

OPENAI_API_KEY=sk-xxxxxxxxxxxx
PINECONE_API_KEY=xxxxxxxxxxxx
PINECONE_ENVIRONMENT=us-west1-gcp
PINECONE_INDEX=my-doc-index

2️⃣ 建立向量資料庫與檢索器

// vectorStore.js
const { PineconeClient } = require("@pinecone-database/pinecone");
const { OpenAIEmbeddings } = require("langchain/embeddings/openai");
const { PineconeStore } = require("langchain/vectorstores/pinecone");

// 初始化 Pinecone
const pinecone = new PineconeClient();
await pinecone.init({
  apiKey: process.env.PINECONE_API_KEY,
  environment: process.env.PINECONE_ENVIRONMENT,
});

const index = pinecone.Index(process.env.PINECONE_INDEX);

// 建立向量儲存 (若已經有資料可直接跳過)
async function initVectorStore() {
  const embeddings = new OpenAIEmbeddings({ openAIApiKey: process.env.OPENAI_API_KEY });
  const vectorStore = await PineconeStore.fromExistingIndex(embeddings, { pineconeIndex: index });
  return vectorStore.asRetriever({ topK: 4 }); // 每次取最相近 4 個片段
}
module.exports = { initVectorStore };

3️⃣ 設定 LLM 與記憶體

// llmAndMemory.js
const { ChatOpenAI } = require("langchain/chat_models/openai");
const { ConversationBufferMemory } = require("langchain/memory");

// 初始化 ChatGPT(使用 gpt‑4o)
const llm = new ChatOpenAI({
  temperature: 0.2,
  modelName: "gpt-4o",
  openAIApiKey: process.env.OPENAI_API_KEY,
});

// 記憶體會自動把對話歷史加入 prompt
const memory = new ConversationBufferMemory({
  memoryKey: "chat_history", // 之後在 prompt 中使用 {{chat_history}}
  returnMessages: true,      // 讓 memory 回傳訊息陣列
});

module.exports = { llm, memory };

4️⃣ 建立 Conversational Retrieval Chain

// ragChain.js
const { ConversationalRetrievalChain } = require("langchain/chains");
const { initVectorStore } = require("./vectorStore");
const { llm, memory } = require("./llmAndMemory");

// 建立 Chain(單例)
async function buildChain() {
  const retriever = await initVectorStore();

  const chain = ConversationalRetrievalChain.fromLLM(llm, retriever, {
    memory,                     // 注入記憶體
    returnSourceDocuments: true // 回傳檢索到的文件,方便除錯
  });

  return chain;
}

module.exports = { buildChain };

5️⃣ 執行對話(示範)

// app.js
const { buildChain } = require("./ragChain");
require("./loadEnv"); // 讀取 .env

async function main() {
  const chain = await buildChain();

  // 第一次提問
  const res1 = await chain.call({ question: "請告訴我 2023 年台北市的平均氣溫。" });
  console.log("🟢 回答:", res1.answer);
  console.log("🔎 參考文件:", res1.sourceDocuments.map(d => d.pageContent).join("\n---\n"));

  // 第二次提問(省略關鍵字)
  const res2 = await chain.call({ question: "那去年呢?" });
  console.log("\n🟢 回答:", res2.answer);
  console.log("🔎 參考文件:", res2.sourceDocuments.map(d => d.pageContent).join("\n---\n"));
}

main().catch(console.error);

執行結果(僅示意):

🟢 回答: 2023 年台北市的平均氣溫為 23.5°C(根據氣象局統計)。
🔎 參考文件: ...
🟢 回答: 2022 年台北市的平均氣溫為 22.9°C,較去年低 0.6°C。
🔎 參考文件: ...

重點:第二次提問只說「那去年呢?」時,記憶體已自動把「台北市」與「平均氣溫」補回檢索詞,讓結果仍然正確。

6️⃣ 自訂 Prompt(進階)

若想讓模型更注重「來源可信度」或「引用格式」,可以自行改寫 Prompt:

// customPrompt.js
const { PromptTemplate } = require("langchain/prompts");

const CONDENSE_QUESTION_PROMPT = PromptTemplate.fromTemplate(`
根據以下對話歷史,請把使用者的最新問題濃縮成一個完整的查詢句子。
對話歷史:
{{chat_history}}
使用者問題: {{question}}
濃縮後的查詢:
`);

module.exports = { CONDENSE_QUESTION_PROMPT };

在 ConversationalRetrievalChain 建構時傳入 condenseQuestionPrompt 即可。


常見陷阱與最佳實踐

陷阱 說明 解決方式
檢索結果太少 向量資料庫的 topK 設太小,或資料本身不足,會導致 LLM 缺乏依據。 先確保文件已完整向量化;適度調高 topK(如 5~10)。
記憶體過長 長時間對話會讓 ConversationBufferMemory 變得龐大,甚至超過 LLM token 限制。 使用 ConversationSummaryMemory 只保留摘要;或在每輪對話後手動裁剪舊訊息。
向量相似度失真 不同語言或專有名詞的向量表現不佳,檢索不到關鍵文件。 針對特定領域微調 Embedding 模型,或在前處理階段加入同義詞擴增。
回覆中未引用來源 使用者需要可追溯的資訊時,模型可能直接「捏造」答案。 在 Prompt 中加入「必須以 [[來源編號]] 方式標註」的指示,並在程式碼中回傳 sourceDocuments。
API 成本失控 每輪檢索 + 生成都會產生多筆 API 請求。 設定 temperature=0、maxTokens 上限;使用快取(如 Redis)儲存相同問題的結果。

最佳實踐:

  1. 先做好資料前處理:切分文件為 300~500 token 的段落,確保向量化後的相似度更精準。
  2. 使用「濃縮問題」的 Prompt:把多輪上下文壓縮成單一檢索查詢,提升檢索品質。
  3. 回傳來源文件:在 UI 上顯示「參考來源」或提供「下載 PDF」連結,提升使用者信任。
  4. 監控成本與效能:結合 langsmith(LangChain 監控平台)觀測每次呼叫的 token 數與延遲。
  5. 安全性檢查:對使用者輸入做基本過濾,防止惡意指令注入向量資料庫。

實際應用場景

場景 需求 Conversational RAG 的價值
客服機器人 即時回應客戶問題,同時引用最新的產品說明書與 FAQ。 能在客戶提問「上次的折扣代碼還能用嗎?」時,自動帶出最新的促銷政策。
企業知識庫助理 員工查詢內部文件、合約條款、技術手冊等。 透過向量檢索,保證答案根據最新版本文件,且對話中自動補全上下文。
醫療諮詢 醫師查閱最新的臨床指南或病例報告。 在多輪討論病歷時,系統會持續把相關文獻拉入上下文,協助醫師做出更精準的判斷(注意合規)。
教育平台 學生提問課堂筆記、教材內容。 系統可根據學生的前後提問,自動補全「哪本書」或「哪章」的資訊,提供完整解說。
法律文件分析 律師需要引用相關判例、條文。 在長對話中,系統會自動把相關條文與案例片段加入,避免遺漏關鍵依據。

總結

Conversational RAG 是結合 即時檢索 與 多輪對話 的強大模式,能讓 LLM 不再只靠「訓練期間的知識」而是依賴最新、可信的外部資訊。透過 LangChain 的模組化設計,我們只需要:

  1. 向量化文件 → 建立 Retriever。
  2. 配置記憶體 → 保持對話上下文。
  3. 組合 Chain → 把檢索、記憶、生成串成完整流程。

加上適當的 Prompt 調校與成本監控,就能在客服、企業知識庫、教育、醫療等眾多場景中,快速打造出 可靠且可追溯 的對話式 AI 服務。希望本篇教學能讓你在實作 Conversational RAG 時少走彎路,快速上手並產出具備商業價值的產品。祝開發順利! 🚀