本文 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+)OpenAIPinecone 作為向量資料庫。所有程式碼都已加上說明註解,請依序執行。

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=0maxTokens 上限;使用快取(如 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 時少走彎路,快速上手並產出具備商業價值的產品。祝開發順利! 🚀