一句話理解
LLM 本身無狀態,每次呼叫都是全新的;Memory 的作用是把對話歷史塞回 prompt,讓 AI「記得」之前說了什麼。
問題根源
# 沒有 Memory 的情況
llm.invoke("我叫大益") # AI: 你好大益!
llm.invoke("我叫什麼名字?") # AI: 我不知道你叫什麼名字... 😅方法一:手動管理歷史(LCEL 推薦)
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
llm = ChatOpenAI(model="gpt-4o-mini")
history = [SystemMessage(content="你是一個友善的助理,請用繁體中文回答。")]
def chat(user_input):
history.append(HumanMessage(content=user_input))
response = llm.invoke(history)
history.append(AIMessage(content=response.content))
return response.content
print(chat("我叫大益")) # 你好大益!
print(chat("我叫什麼名字?")) # 你叫大益!方法二:ChatMessageHistory
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# 每個 session 一個 history 物件
store = {}
def get_session_history(session_id: str):
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
# Prompt 留一個位置給 history
prompt = ChatPromptTemplate.from_messages([
("system", "你是友善的助理"),
MessagesPlaceholder(variable_name="history"), # history 插入這裡
("human", "{input}")
])
llm = ChatOpenAI(model="gpt-4o-mini")
chain = prompt | llm
# 包裝成有記憶的 chain
chain_with_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="history"
)
# 用 session_id 區分不同對話
config = {"configurable": {"session_id": "user_123"}}
chain_with_history.invoke({"input": "我叫大益"}, config=config)
chain_with_history.invoke({"input": "我叫什麼名字?"}, config=config)方法三:持久化到 Redis(生產環境)
from langchain_community.chat_message_histories import RedisChatMessageHistory
def get_session_history(session_id: str):
return RedisChatMessageHistory(
session_id=session_id,
url="redis://localhost:6379"
)
# 其餘與方法二相同記憶體用量控制
長對話會讓 token 暴增,需要截斷:
from langchain_core.chat_history import InMemoryChatMessageHistory
class TrimmedChatHistory(InMemoryChatMessageHistory):
max_messages: int = 10 # 只保留最近 10 條
def add_message(self, message):
super().add_message(message)
# 超過上限就刪最舊的(保留 system message)
if len(self.messages) > self.max_messages:
self.messages = self.messages[-self.max_messages:]或用 LangChain 內建的 trim_messages:
from langchain_core.messages import trim_messages
trimmed = trim_messages(
messages=history,
max_tokens=2000, # 限制 token 數
token_counter=llm, # 用 LLM 數 token
strategy="last", # 保留最後的訊息
include_system=True # 永遠保留 system message
)對話記憶類型比較
| 類型 | 做法 | token 用量 | 適合 |
|---|---|---|---|
| Full History | 全部歷史都傳 | 隨對話增長 | 短對話 |
| Window | 只傳最近 N 條 | 固定 | 一般對話 |
| Summary | 摘要舊對話 + 最近 N 條 | 較少 | 長對話 |
| Redis 持久化 | 存到 Redis | 不佔記憶體 | 生產環境 |
常見錯誤
| 錯誤 | 原因 | 解法 |
|---|---|---|
| AI 還是記不住 | session_id 每次不同 | 確認 config session_id 一致 |
| Token 超限錯誤 | History 太長 | 用 trim_messages 截斷 |
| 多使用者資料混用 | 共用同一個 history 物件 | 每個 session 各自建立 |
相關筆記
- LangChain — 模組地圖
- LangChain-基礎Chain — LCEL 基礎
- LangChain-Agent — Agent 也需要 Memory