本文 AI 產出,尚未審核

LangChain 課程 – Memory:對話記憶

主題:使用向量庫的長期記憶


簡介

在聊天機器人或 AI 助手的開發過程中,記憶是讓對話更自然、連貫的關鍵。
傳統的短期記憶(如 ConversationBufferMemory)只能保存最近幾輪的對話內容,當資訊量增大、對話歷史變長時,模型會遺忘先前的重要細節。這時 向量庫(Vector Store) 的長期記憶就派上用場:把過去的對話或文件嵌入成向量,存入向量資料庫,之後再以相似度搜尋的方式快速找回相關資訊,讓模型彷彿「有了自己的筆記本」。

本篇文章將以 LangChainJS 為例,說明如何建立、操作、整合向量庫作為長期記憶,並提供實作範例、常見陷阱與最佳實踐,讓你能在自己的應用中快速上手。


核心概念

1. 向量嵌入(Embedding)與向量庫(Vector Store)

  • Embedding:將文字轉換為固定長度的向量,向量之間的距離(如餘弦相似度)代表語意相似度。
  • Vector Store:用於儲存與檢索向量的資料庫,常見的實作有 FAISS、Pinecone、Weaviate、Qdrant 等。

在 LangChain 中,我們只需要提供一個 Embeddings 物件與一個 VectorStore 類別,即可完成「文字 ↔ 向量」的雙向轉換與搜尋。

2. 長期記憶的工作流程

  1. 將對話或文件切分(Chunk)成適當大小的片段。
  2. 產生 Embedding:使用 OpenAI、Cohere、HuggingFace 等模型把每個片段編碼成向量。
  3. 寫入 Vector Store:把向量與原始文字一起存入資料庫。
  4. 檢索:當新問題來時,先把問題嵌入向量,再在向量庫中找出最相似的片段,作為「記憶」提供給 LLM。
  5. 組合回應:將檢索結果與當前對話歷史一起送入 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,可能被惡意利用。 使用 templateLLMChain,將變數安全注入;避免直接拼接未過濾文字。

最佳實踐小結

  1. 先切分再嵌入:使用 RecursiveCharacterTextSplitter 把長文件切成 500~1000 token 片段,降低向量噪聲。
  2. 分層檢索:先用 BM25(關鍵字)快速過濾,再用向量相似度精緻排序,能兼顧速度與語意。
  3. 定期清理:舊的、過時的記憶會干擾模型,使用 metadata 標記過期時間,定期執行 delete
  4. 監控 Token 使用:在 Memory 中加入 tokenCounter,即時掌握每次請求的 token 數量。
  5. 安全性:對所有使用者輸入做基本過濾(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 的對話系統不再受限於短暫的聊天窗口,而是能像人類一樣,隨時調出過去的資訊、文件與經驗。透過以下步驟即可完成:

  1. 切分 → 2. 嵌入 → 3. 寫入 Vector Store → 4. 以 Retriever 作為 Memory → 5. 結合 LLM 產生回應

在實務上,配合 短期記憶(ConversationBuffer)與 向量長期記憶,再加入適當的 metadata分層檢索成本控制,即可打造出高效、可擴展且安全的 AI 對話應用。

希望本篇文章能幫助你快速上手向量長期記憶,將 LangChain 的力量發揮到最大!祝開發順利 🎉。