LangChain 課程 – Memory:對話記憶
主題:使用向量庫的長期記憶
簡介
在聊天機器人或 AI 助手的開發過程中,記憶是讓對話更自然、連貫的關鍵。
傳統的短期記憶(如 ConversationBufferMemory)只能保存最近幾輪的對話內容,當資訊量增大、對話歷史變長時,模型會遺忘先前的重要細節。這時 向量庫(Vector Store) 的長期記憶就派上用場:把過去的對話或文件嵌入成向量,存入向量資料庫,之後再以相似度搜尋的方式快速找回相關資訊,讓模型彷彿「有了自己的筆記本」。
本篇文章將以 LangChainJS 為例,說明如何建立、操作、整合向量庫作為長期記憶,並提供實作範例、常見陷阱與最佳實踐,讓你能在自己的應用中快速上手。
核心概念
1. 向量嵌入(Embedding)與向量庫(Vector Store)
- Embedding:將文字轉換為固定長度的向量,向量之間的距離(如餘弦相似度)代表語意相似度。
- Vector Store:用於儲存與檢索向量的資料庫,常見的實作有 FAISS、Pinecone、Weaviate、Qdrant 等。
在 LangChain 中,我們只需要提供一個 Embeddings 物件與一個 VectorStore 類別,即可完成「文字 ↔ 向量」的雙向轉換與搜尋。
2. 長期記憶的工作流程
- 將對話或文件切分(Chunk)成適當大小的片段。
- 產生 Embedding:使用 OpenAI、Cohere、HuggingFace 等模型把每個片段編碼成向量。
- 寫入 Vector Store:把向量與原始文字一起存入資料庫。
- 檢索:當新問題來時,先把問題嵌入向量,再在向量庫中找出最相似的片段,作為「記憶」提供給 LLM。
- 組合回應:將檢索結果與當前對話歷史一起送入 LLM,產生最終答案。
下面的程式碼範例會一步步展示這個流程。
程式碼範例
註:以下範例使用
langchainjs(v0.1+),Node.js 環境,請先安裝npm i langchain openai @langchain/community faiss-node。
1️⃣ 建立 Embedding 與 Vector Store(FAISS 為例)
// import 必要模組
import { OpenAIEmbeddings } from "@langchain/openai";
import { FAISS } from "@langchain/community/vectorstores/faiss";
import { Document } from "@langchain/document";
// 初始化 OpenAI Embedding(需要設定 OPENAI_API_KEY)
const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
});
// 假設有一批歷史對話或說明文件
const rawTexts = [
"使用者 A:請問今天的天氣如何?",
"系統:今天台北晴,最高溫度 28°C。",
"使用者 B:我想知道什麼是向量搜尋?",
"系統:向量搜尋是透過向量相似度找出語意相近的資料。",
];
// 轉成 Document 物件,方便後續切分
const docs = rawTexts.map((t) => new Document({ pageContent: t }));
// 建立 FAISS 向量庫(會自動產生 Embedding 並寫入)
const vectorStore = await FAISS.fromDocuments(docs, embeddings);
說明:
Document讓我們可以同時保存文字與未來可能的 metadata(如來源、時間戳記)。FAISS.fromDocuments會自動把每個Document轉成向量,並建立索引,省去手動addVectors的步驟。
2️⃣ 在對話中使用長期記憶(Memory)
import { ConversationChain } from "@langchain/chains";
import { OpenAI } from "@langchain/openai";
import { VectorStoreRetrieverMemory } from "@langchain/memory";
// 建立一個 Retriever,使用向量庫的相似度搜尋
const retriever = vectorStore.asRetriever({
// 每次取回最相似的 3 個片段
k: 3,
});
// 把 Retriever 包裝成 Memory
const memory = new VectorStoreRetrieverMemory({
retriever,
// 記憶的 key,LLM 會以 `context` 參數取得
memoryKey: "context",
});
// 建立 LLM 與 ConversationChain
const llm = new OpenAI({ temperature: 0 });
const chain = new ConversationChain({
llm,
memory,
});
// 測試對話
const response1 = await chain.call({ input: "向量搜尋的原理是什麼?" });
console.log(response1.response);
// 輸出會結合檢索到的相關片段,提供更完整的說明
重點:
VectorStoreRetrieverMemory會在每次呼叫 LLM 前,自動把retriever搜尋到的文字拼接成context,讓模型能「看到」過去的資訊。k的設定直接影響檢索結果的數量,過大會增加 token 費用,過小則可能遺漏關鍵資訊。
3️⃣ 動態寫入新記憶(持續學習)
// 假設使用者剛說了一段新資訊
const newInfo = "系統:2025 年台北的平均氣溫將上升至 30°C。";
// 把新資訊寫入向量庫
await vectorStore.addDocuments([new Document({ pageContent: newInfo })]);
// 為了讓新向量立即可被搜尋,重新建立 retriever
const updatedRetriever = vectorStore.asRetriever({ k: 3 });
memory.retriever = updatedRetriever; // 更新 Memory 內的 retriever
// 再次詢問
const response2 = await chain.call({ input: "未來台北的氣溫會變高嗎?" });
console.log(response2.response);
說明:
addDocuments會自動產生嵌入並加入索引。- 若使用 FAISS,加入新向量後不需要手動重建索引;但對於遠端服務(如 Pinecone)可能需要
await vectorStore.refresh()。
4️⃣ 使用遠端向量服務(Pinecone 範例)
import { PineconeStore } from "@langchain/community/vectorstores/pinecone";
import { PineconeClient } from "@pinecone-database/pinecone";
// 初始化 Pinecone
const pinecone = new PineconeClient();
await pinecone.init({
environment: "us-west1-gcp", // 依照你的環境設定
apiKey: process.env.PINECONE_API_KEY,
});
const index = pinecone.Index("langchain-demo");
// 建立 PineconeStore
const pineconeStore = await PineconeStore.fromDocuments(docs, embeddings, {
pineconeIndex: index,
namespace: "conversation-mem",
});
// 使用方式與 FAISS 完全相同,只是底層改為遠端服務
const pineconeRetriever = pineconeStore.asRetriever({ k: 4 });
提示:遠端向量服務適合 大量資料(百萬級以上)與 多租戶 的情境,且支援即時擴容與持久化存儲。
5️⃣ 結合多種記憶(短期 + 長期)
import { ConversationBufferMemory } from "@langchain/memory";
// 短期記憶:只保存最近 5 條對話
const shortMemory = new ConversationBufferMemory({
memoryKey: "chat_history",
returnMessages: true,
inputKey: "input",
outputKey: "output",
maxTokenLimit: 500,
});
// 長期記憶:向量檢索
const longMemory = new VectorStoreRetrieverMemory({
retriever: vectorStore.asRetriever({ k: 3 }),
memoryKey: "context",
});
// 建立自訂 Chain,將兩種記憶合併傳給 LLM
const customChain = async ({ input }) => {
// 取得短期聊天歷史
const chatHist = await shortMemory.loadMemoryVariables({});
// 取得長期向量上下文
const ctx = await longMemory.loadMemoryVariables({});
const prompt = `
你是一位助理,請根據以下資訊回答使用者的問題。
**短期對話歷史**:
${chatHist.chat_history?.map((m) => m.content).join("\n")}
**長期相關記憶**:
${ctx.context}
使用者問題: ${input}
`;
const answer = await llm.invoke(prompt);
// 更新短期記憶
await shortMemory.saveContext({ input }, { output: answer });
return { response: answer };
};
const result = await customChain({ input: "2025 年台北的天氣會怎樣?" });
console.log(result.response);
關鍵:
- 透過
ConversationBufferMemory捕捉最新的對話脈絡,避免每次都重新檢索全部長期記憶。maxTokenLimit可以控制短期記憶的大小,防止 Prompt 超過模型上限。
常見陷阱與最佳實踐
| 陷阱 | 為什麼會發生 | 解決方案 / 實踐 |
|---|---|---|
| 向量維度不一致 | 不同 Embedding 模型產生的向量長度不同,導致向量庫建立失敗。 | 統一使用同一個 Embeddings 實例;若必須切換模型,請重新建構 Vector Store。 |
| 檢索結果過多導致 Token 爆炸 | k 設定太大,或檢索到的文字過長。 |
設定合理的 k(3~5 為常見),並在返回前使用 textSplitter 只保留關鍵片段。 |
| 向量庫未同步更新 | 新增文件後忘記刷新 Retriever,導致舊資料仍被使用。 | 在遠端服務(Pinecone、Weaviate)使用 await vectorStore.refresh();FAISS 自動同步。 |
| 忘記加入 Metadata | 後續需要根據來源、時間篩選時無法做到。 | 建立 Document 時加入 metadata,如 { source: "user", timestamp: Date.now() },在檢索時利用 filter。 |
| Embedding 成本過高 | 大量文件一次性全部嵌入,會產生高額 API 費用。 | 採用 批次處理(batch size 10~20),或先在本地跑開源模型(如 sentence-transformers)。 |
| Prompt 注入攻擊 | 直接把使用者輸入拼接到 Prompt,可能被惡意利用。 | 使用 template 或 LLMChain,將變數安全注入;避免直接拼接未過濾文字。 |
最佳實踐小結
- 先切分再嵌入:使用
RecursiveCharacterTextSplitter把長文件切成 500~1000 token 片段,降低向量噪聲。 - 分層檢索:先用 BM25(關鍵字)快速過濾,再用向量相似度精緻排序,能兼顧速度與語意。
- 定期清理:舊的、過時的記憶會干擾模型,使用
metadata標記過期時間,定期執行delete。 - 監控 Token 使用:在
Memory中加入tokenCounter,即時掌握每次請求的 token 數量。 - 安全性:對所有使用者輸入做基本過濾(HTML、SQL、腳本),防止 Prompt Injection。
實際應用場景
| 場景 | 為何需要向量長期記憶 | 實作要點 |
|---|---|---|
| 客服機器人 | 必須記住客戶過去的訂單、問題歷史,才能提供個人化回應。 | 把每筆客服對話存入 Vector Store,使用 metadata 標註客戶 ID,檢索時加上 filter: { customerId: "12345" }。 |
| 企業內部知識庫 | 員工查詢常見問題或 SOP 時,資料量龐大且不斷更新。 | 以文件(PDF、Word)為來源,批次嵌入至 Pinecone,結合關鍵字檢索提升速度。 |
| 教育輔助系統 | 學生的提問與老師的回覆需要跨課程累積,形成個人化學習歷程。 | 每堂課的 QA 存入向量庫,使用 timestamp 做時間篩選,讓模型回答時能參照最近的學習階段。 |
| 醫療諮詢助理 | 病患的過往病史、檢查報告必須在對話中被正確引用。 | 把醫療報告切成小段,加入 metadata: { patientId, reportDate },檢索時根據 patientId 及日期範圍過濾。 |
| 遊戲 NPC 對話 | NPC 必須記得玩家過去的互動,讓劇情更有深度。 | 把玩家的選項與 NPC 回應存入向量庫,使用 k=1 只取最近一次相關對話,保持敘事連貫。 |
總結
向量庫提供的 長期記憶,讓 LangChain 的對話系統不再受限於短暫的聊天窗口,而是能像人類一樣,隨時調出過去的資訊、文件與經驗。透過以下步驟即可完成:
- 切分 → 2. 嵌入 → 3. 寫入 Vector Store → 4. 以 Retriever 作為 Memory → 5. 結合 LLM 產生回應。
在實務上,配合 短期記憶(ConversationBuffer)與 向量長期記憶,再加入適當的 metadata、分層檢索、成本控制,即可打造出高效、可擴展且安全的 AI 對話應用。
希望本篇文章能幫助你快速上手向量長期記憶,將 LangChain 的力量發揮到最大!祝開發順利 🎉。