一句話理解

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 各自建立

相關筆記