🧠 實戰對話記憶體 (Memory):讓客服機器人記住上下文

如果你跟著前幾章把 RAG (檢索增強生成) 系統做好了,你可能會開心地去測試:

你:「嗨,我想請問 Vibe Tutor 網站的年費是多少?」 AI:「Vibe Tutor 的尊榮會員年費是 $299 美金。」 你:「那如果我現在刷卡,有打折嗎?」 AI:「請問您指的是哪一項產品的打折呢?」

氣死人! 明明上一句才剛講到 Vibe Tutor,下一句它就忘光了! 這就是所有新手做 AI 機器人時會遇到的第一個大坑:LLM (大型語言模型) 本身是「無狀態 (Stateless)」的,它就像金魚一樣,只有 7 秒鐘的記憶。

本章將教你如何使用 LangChain 實作記憶體 (Memory) 機制,讓你的機器人變得像真人客服一樣聰明。


1. 為什麼 AI 會忘記?

當你呼叫 openai.chat.completions.create() 時,API 是獨立的。 不管你五分鐘前傳了什麼給它,只要你這次沒有傳過去,它就絕對不知道。

要讓 AI「有記憶」的唯一方法,就是: 「每一次對話時,你都要把『過去所有的對話紀錄』當成附檔,一起貼給 AI 看!」

這聽起來很笨,但這就是 ChatGPT 網頁版背後運作的真實原理。


2. LangChain 的 BufferMemory

如果你要自己手寫程式碼去管理這些對話紀錄:

let history = [];
history.push({ role: 'user', content: '你好' });
history.push({ role: 'ai', content: '你好,我是客服' });
// 下次呼叫又要把它們 map 出來塞進 prompt 裡...

這會非常痛苦。好險,LangChain 提供了一套名為 Memory 的元件來幫我們做這件事。

最基礎也最常用的,就是 BufferMemory。它會把你過去的對話,轉成一個很長很長的字串,塞進 Prompt 的 {history} 變數裡。

實作記憶體客服對話

import { ChatOpenAI } from "langchain/chat_models/openai";
import { BufferMemory } from "langchain/memory";
import { ConversationChain } from "langchain/chains";
import { PromptTemplate } from "langchain/prompts";

// 1. 設定你的語言模型
const llm = new ChatOpenAI({ temperature: 0.7, modelName: 'gpt-3.5-turbo' });

// 2. 建立記憶體物件
// returnMessages: false 代表它會把歷史轉成一個大字串 (適合放進字串 prompt)
const memory = new BufferMemory({
  memoryKey: "chat_history", 
  returnMessages: false
});

// 3. 設計有記憶點的 Prompt 模板
const prompt = PromptTemplate.fromTemplate(`
你是一位親切的客服人員。以下是你與這位客戶的歷史對話:

{chat_history}

客戶最新說的一句話是:{input}
請給予親切的回覆:
`);

// 4. 把 LLM, Prompt 還有 Memory 綁在一個 Chain 裡面
const chain = new ConversationChain({
  llm: llm,
  memory: memory,
  prompt: prompt
});

// 5. 開始對話!
async function runConversation() {
  const res1 = await chain.call({ input: "你好,我叫做 Ken,我有一隻狗叫做布丁。" });
  console.log("AI:", res1.response);
  // AI: 你好 Ken!有隻叫做布丁的狗聽起來好可愛!請問今天有什麼我可以幫忙的嗎?

  const res2 = await chain.call({ input: "我忘記我的狗叫什麼名字了,你可以提醒我嗎?" });
  console.log("AI:", res2.response);
  // AI: 當然可以!您的狗叫做布丁喔!
}

runConversation();

這時候 AI 就有記憶了!因為 ConversationChain 會在背後自動幫你把 res1 的對話紀錄儲存進 memory 物件裡,然後在呼叫 res2 時把這串紀錄補到 {chat_history} 中。


3. 記憶體爆炸危機:BufferWindowMemory

BufferMemory 有一個致命缺點:Token 會越來越貴,而且很快就會超過限制!

假設你跟機器人聊了 100 句,這 100 句話會全部被傳給 OpenAI,你不只會被收取極高的 Token 費用,還可能直接撞到模型的輸入上限 (例如 8K 或 16K Token)。

為了解決這個問題,業界最常用的其實是 BufferWindowMemory (滑動視窗記憶體)

它只會記住「最近 N 輪的對話」,太舊的直接捨棄。這非常符合人類對話的習慣,因為客服也不需要知道你們三個小時前的寒暄。

import { BufferWindowMemory } from "langchain/memory";

// k: 5 代表只記住「最近 5 輪 (也就是一問一答算 1 輪,共 10 句話)」
const slidingMemory = new BufferWindowMemory({
  k: 5, 
  memoryKey: "chat_history"
});

4. 把記憶力與 RAG 結合:ConversationalRetrievalChain

如果我們不只是要「瞎聊」,而是要他「讀取我們的知識庫 (RAG)」,然後還要「有記憶」,那該怎麼做? 這時候情況會變得很複雜: 因為如果你問「那它有打折嗎?」,我們拿「那它有打折嗎?」去向量資料庫搜尋,絕對搜不到任何東西!(因為它少了『Vibe Tutor 年費』這個關鍵字)。

這時候,LangChain 提供了一個超級英雄:ConversationalRetrievalChain

它的運作原理分兩步:

  1. 它會把你的「聊天紀錄」跟「最新一句模糊的話」,一起交給一個小模型,請他把這句話**「改寫 (Condense Question)」**。 例如:把「那它有打折嗎?」改寫成 ->「Vibe Tutor 尊榮會員年費有打折嗎?」
  2. 它拿改寫後、超清楚的這句話,去向量資料庫搜尋出相關文章。
  3. 最後再把文章跟對話紀錄一起交給大模型生成答案。

實戰最終版 RAG 客服:

import { ConversationalRetrievalQAChain } from "langchain/chains";
import { BufferWindowMemory } from "langchain/memory";

// 假設你已經有了 vectorStore
const retriever = vectorStore.asRetriever();
const llm = new ChatOpenAI({ temperature: 0 });

const memory = new BufferWindowMemory({
  k: 5,
  memoryKey: "chat_history", // 必須是 chat_history
  returnMessages: true,      // 讓它回傳對話陣列格式
});

// 建立具有對話記憶的 RAG Chain
const chatChain = ConversationalRetrievalQAChain.fromLLM(
  llm,
  retriever,
  {
    memory: memory,
    // (可選) 你甚至可以客製化「改寫問題」的 Prompt
    questionGeneratorChainOptions: {
      template: `給定以下對話紀錄和一個後續問題,請將後續問題改寫為一個獨立的、意思完整的單一問題。
      對話紀錄:
      {chat_history}
      後續問題:{question}
      獨立問題:`
    }
  }
);

// 執行!
const result = await chatChain.call({ question: "那如果我現在刷卡,有打折嗎?" });
console.log(result.text);

有了 ConversationalRetrievalQAChain,你的知識庫 RAG 系統就不再是一個「一問一答的搜尋引擎」,而是一個「有溫度、有連貫性、能進行深度對話探討」的超級助教! 趕快把這套機制導入你的 Vibe Tutor 網站吧!

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!