🧠 實戰對話記憶體 (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。
它的運作原理分兩步:
- 它會把你的「聊天紀錄」跟「最新一句模糊的話」,一起交給一個小模型,請他把這句話**「改寫 (Condense Question)」**。 例如:把「那它有打折嗎?」改寫成 ->「Vibe Tutor 尊榮會員年費有打折嗎?」
- 它拿改寫後、超清楚的這句話,去向量資料庫搜尋出相關文章。
- 最後再把文章跟對話紀錄一起交給大模型生成答案。
實戰最終版 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 網站吧!