LangChain 教學:Conversational RAG(對話式檢索增強生成)
簡介
在資訊爆炸的時代,單純的大型語言模型(LLM)仍然會受到「知識截止」與「資訊可信度」的限制。Retrieval‑Augmented Generation(RAG) 透過即時檢索外部文件,讓模型的回答能夠根據最新、最相關的資料產出。
當我們把 RAG 與對話流程結合,就形成 Conversational RAG,即在多輪對話中持續利用檢索結果作為上下文,提升答案的正確性與一致性。這樣的技術在客服機器人、企業內部知識庫、教育助教等場景裡,能顯著降低誤答率、提升使用者體驗。
本篇文章將以 LangChain(JavaScript 版)為例,說明如何在 Node.js 環境下快速構建 Conversational RAG,並提供實作範例、常見陷阱與最佳實踐,幫助你從零開始打造可商用的對話式檢索系統。
核心概念
1. RAG 基本流程
- 檢索(Retrieval):根據使用者的提問,從向量資料庫或全文搜索引擎取出最相關的文件片段。
- 整合(Augmentation):將檢索到的片段與對話歷史合併,形成「擴充上下文」。
- 生成(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)儲存相同問題的結果。 |
最佳實踐:
- 先做好資料前處理:切分文件為 300~500 token 的段落,確保向量化後的相似度更精準。
- 使用「濃縮問題」的 Prompt:把多輪上下文壓縮成單一檢索查詢,提升檢索品質。
- 回傳來源文件:在 UI 上顯示「參考來源」或提供「下載 PDF」連結,提升使用者信任。
- 監控成本與效能:結合
langsmith(LangChain 監控平台)觀測每次呼叫的 token 數與延遲。 - 安全性檢查:對使用者輸入做基本過濾,防止惡意指令注入向量資料庫。
實際應用場景
| 場景 | 需求 | Conversational RAG 的價值 |
|---|---|---|
| 客服機器人 | 即時回應客戶問題,同時引用最新的產品說明書與 FAQ。 | 能在客戶提問「上次的折扣代碼還能用嗎?」時,自動帶出最新的促銷政策。 |
| 企業知識庫助理 | 員工查詢內部文件、合約條款、技術手冊等。 | 透過向量檢索,保證答案根據最新版本文件,且對話中自動補全上下文。 |
| 醫療諮詢 | 醫師查閱最新的臨床指南或病例報告。 | 在多輪討論病歷時,系統會持續把相關文獻拉入上下文,協助醫師做出更精準的判斷(注意合規)。 |
| 教育平台 | 學生提問課堂筆記、教材內容。 | 系統可根據學生的前後提問,自動補全「哪本書」或「哪章」的資訊,提供完整解說。 |
| 法律文件分析 | 律師需要引用相關判例、條文。 | 在長對話中,系統會自動把相關條文與案例片段加入,避免遺漏關鍵依據。 |
總結
Conversational RAG 是結合 即時檢索 與 多輪對話 的強大模式,能讓 LLM 不再只靠「訓練期間的知識」而是依賴最新、可信的外部資訊。透過 LangChain 的模組化設計,我們只需要:
- 向量化文件 → 建立
Retriever。 - 配置記憶體 → 保持對話上下文。
- 組合 Chain → 把檢索、記憶、生成串成完整流程。
加上適當的 Prompt 調校與成本監控,就能在客服、企業知識庫、教育、醫療等眾多場景中,快速打造出 可靠且可追溯 的對話式 AI 服務。希望本篇教學能讓你在實作 Conversational RAG 時少走彎路,快速上手並產出具備商業價值的產品。祝開發順利! 🚀