18-day5-memory
🧠 Day5 理论:让 AI Agent 拥有“记忆”——对话记录

1. 问题背景:为什么需要“记忆”?
默认情况下,大语言模型(如 Qwen、GPT)是无状态的:
每次你发送一条消息,它都当作“第一次对话”来处理,不会记住你之前说过什么。
例如:
你说:“我叫小明。”
下一句问:“我叫什么?”
→ 模型很可能回答:“我不知道。”
这在真实对话中是不可接受的。
因此,我们需要一种机制,把之前的对话内容“带入”当前请求,这就是 对话记忆(Chat Memory)。
2. 核心思想:把历史消息作为上下文传给模型
实现记忆的关键不是让模型“真的记住”,而是:
在每次提问时,把之前的对话记录(历史)一起发给模型。
这样,模型就能基于完整上下文生成连贯的回答。
例如:
[系统] 你是一个有记忆的助手。
[用户] 我叫黄河。
[助手] 你好,黄河!
[用户] 我叫什么名字?
→ 把前三条消息都发给模型 → 模型知道答案是“黄河”
day5-让 Agent 记住你说过的话. 单用户
1️⃣ 创建工作目录 & 虚拟环境
# 创建目录
mkdir -p day5_memory && cd day5_memory
# 创建虚拟环境(Python 3.10+)
python3 -m venv day5-memory
source day5-memory/bin/activate
# 升级 pip
pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple/
# 创建 .env 文件(替换为您的实际API Key)
echo "DASHSCOPE_API_KEY=sk-6adfbafe2c854374b32c720982666ce5" > .env
✅
pip install langchain langchain-community langchain-openai python-dotenv -i https://mirrors.aliyun.com/pypi/simple/
tee agent_with_memory-1.py <<'EOF'
# agent_with_memory_1.py
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
# 加载 .env
load_dotenv()
# 初始化模型(DashScope 兼容 OpenAI)
llm = ChatOpenAI(
model="qwen-max",
openai_api_key=os.getenv("DASHSCOPE_API_KEY"),
openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
temperature=0.7
)
# Prompt 模板
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个有记忆的 AI 助手。请记住用户之前说过的话。"),
MessagesPlaceholder(variable_name="history"), # 👈【MEMORY:先放历史】
("human", "{input}") # 👈【再放当前问题】
])
# LangChain 表达式,使用 Python 的 “管道操作符” |
# 将两个组件 串联成一个可运行的链(Runnable Chain)
chain = prompt | llm
# 内存存储历史 👈 ←←←【MEMORY 核心:会话历史的全局存储字典】
store = {}
# 获取/创建会话历史
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory() # 👈 ←←←【MEMORY:首次访问时创建新历史对象】
return store[session_id] # 👈 ←←←【MEMORY:返回对应会话的历史对象】
# 绑定记忆到链
with_message_history = RunnableWithMessageHistory(
chain,
get_session_history, # 👈 ←←←【MEMORY:指定如何根据 session_id 获取历史】
input_messages_key="input", # 用户输入字段名
history_messages_key="history", # 👈 ←←←【MEMORY:指定 prompt 中占位符变量名,必须与 MessagesPlaceholder 一致】
)
if __name__ == "__main__":
print("🤖 记忆型 Agent 启动!输入 'quit' 退出,输入 'show_memory' 查看当前记忆。\n")
while True:
user_input = input("👤 你: ")
if user_input.strip().lower() == "quit":
break
elif user_input.strip().lower() == "show_memory":
# 打印当前 session 的记忆
history = get_session_history("abc123") # 👈 ←←←【MEMORY:获取 ID 为 "abc123" 的会话历史】
print("🧠 当前记忆内容:")
for msg in history.messages:
role = "👤 用户" if msg.type == "human" else "🤖 Agent"
print(f"{role}: {msg.content}")
print()
continue
response = with_message_history.invoke(
{"input": user_input},
config={"configurable": {"session_id": "abc123"}} # 👈 ←←←【MEMORY:本次调用所属的会话 ID,用于自动加载/保存历史】
)
print(f"🤖 Agent: {response.content}\n")
EOF
python agent_with_memory-1.py
测试
(day5-memory) [jacky@vms106 day5_memory]$python agent_with_memory.py
🤖 记忆型 Agent 启动!输入 'quit' 退出。
👤 你: 我叫张月山
🤖 Agent: 你好,张月山!很高兴认识你。如果你有任何问题或需要帮助,请随时告诉我。
👤 你: 我叫什么名字
🤖 Agent: 你叫张月山。如果你有其他问题或需要帮助,随时告诉我哦!
👤 你: 现在几点
🤖 Agent: 我目前无法直接查看实时时间。你可以查看设备上的时间来获取准确的时间。如果需要帮助设置时钟或有其他问题,随时告诉我!
👤 你: 我可以问你什么问题
🤖 Agent: 你可以问我各种各样的问题,包括但不限于以下几类:
1. **知识类问题**:如历史、科学、技术、文化、艺术等方面的问题。
2. **实用信息**:如天气预报、新闻更新、时间转换等。
3. **生活建议**:如健康饮食、锻炼建议、旅行规划等。
4. **学习帮助**:如语言学习、数学问题、编程问题等。
5. **娱乐**:如电影推荐、音乐推荐、谜语或小游戏等。
6. **技术支持**:如设备使用问题、软件安装和配置等。
7. **心理支持**:如情绪管理、压力缓解等。
如果你有任何具体的问题或需要帮助的地方,请随时告诉我!
👤 你: 1+1等于几 ?
🤖 Agent: 1+1等于2。如果你有其他数学问题或需要帮助的地方,随时告诉我!
👤 你: 刚刚我问你什么
🤖 Agent: 你刚刚问我“1+1等于几”,我回答说1+1等于2。如果你有其他问题或需要进一步的帮助,请告诉我!
带记忆的 Agent 工作过程**
一、先拥有“短期记忆”
大型语言模型(如 Qwen-Max)本身是无状态的:每次调用都是独立的,无法自动记住上一轮对话。
要实现记忆功能,必须在应用层显式地管理历史消息,并将其作为上下文输入给模型。
本文的程序通过 LangChain 的 RunnableWithMessageHistory 和 ChatMessageHistory 实现了这一能力,
其本质是:
在每次调用 LLM 前,把过去的对话记录拼接到 Prompt 中,让模型“看到”历史。
二、记忆机制的核心组件
🏫 比喻:用学生管理系统 阐述技术组件
学校要记住他们全程的学习情况、提问记录和老师反馈。
1. store:全校学生的「电子学籍档案库」
一个中央数据库,按学号存储每个学生的完整成长档案。
- 每位学生有唯一 学号(session_id),如
"S2026001"。 - 档案库里每个学号对应一个 个人成长记录本(
ChatMessageHistory),记录所有对话:- 学生提问:“这道题怎么做?”
- 老师(AI)回答:“我们先看公式……”
- 新生入学?系统自动创建空白档案。
✅ 作用:集中、隔离地保存所有学生的交互历史,一人一档。
2. get_session_history(session_id):教务处档案管理员
老师要辅导学生前,必须向管理员申请该生的档案。
- 老师说:“我要查学号 S2026001 的档案。”
- 管理员去档案库(
store)查找:- 找到了 → 拿出记录本;
- 找不到(新生)→ 立即新建一本,编号归档,再交给老师。
- 老师用完后,管理员负责把更新后的档案放回原位。
✅ 作用:根据学号安全、准确地获取或初始化学生记忆档案。
3. MessagesPlaceholder("history"):教师备课模板中的「学生历史栏」
老师每次备课(生成回答)前,都要参考一个标准教案模板。
模板长这样:
[系统角色] 你是一位耐心的数学老师。
[📚 学生历史记录] ←← 这里要粘贴从档案拿来的对话记录
[当前问题] 学生问:“上次那道几何题我还有点不懂。”
- “学生历史记录”是一个预留插槽,不能写死内容。
- 教务系统会自动把档案里的内容填进去,确保老师了解上下文。
✅ 作用:在 Prompt 中明确指定“历史信息插入位置”,让 AI 看到上下文。
4. RunnableWithMessageHistory:智能教学调度平台
学校部署的 AI 教学中台系统,自动协调档案、教师、学生三方。
当学生提交问题时,平台自动完成以下流程:
- 识别学号(从请求中提取
session_id); - 调用档案管理员(
get_session_history)取出该生档案; - 填充教案模板:把历史对话塞进
MessagesPlaceholder("history"); - 调用 AI 教师模型 生成个性化回答;
- 更新档案:将本次问答追加到学生记录本,并存回档案库。
✅ 作用:
全自动实现“加载记忆 → 生成回答 → 保存新记忆”的闭环,
是整个系统的“胶水”与引擎。
🔄 完整流程示例
学生(学号 S2026001)问:“我昨天问的那道二次函数题,能再讲一遍吗?”
- 教学平台收到请求,识别学号
S2026001; - 调用档案管理员,从学籍库取出该生档案(含昨日对话);
- 将历史记录插入教师教案的“学生历史栏”;
- 教师看到上下文,给出这次的回答;
- 平台把这次问答追加到学生档案,归档保存——记忆更新完成!
📚 对照表:技术 ↔ 学籍管理
| 技术组件 | 学籍系统角色 | 核心职责 |
|---|---|---|
store |
全校电子学籍档案库 | 按学号存储所有学生对话历史 |
get_session_history |
教务处档案管理员 | 根据学号取/建学生档案 |
MessagesPlaceholder("history") |
教师教案中的“学生历史”插槽 | 指定上下文插入位置 |
RunnableWithMessageHistory |
智能教学调度平台 | 自动串联“取档 → 备课 → 授课 → 归档”全流程 |
专业阐述
1. store:会话历史的全局存储字典
store = {}
- 这是一个 Python 字典,键为
session_id(如"abc123"),值为ChatMessageHistory对象。 - 每个会话 ID 对应一个独立的对话历史,支持多用户或多轮会话隔离。
2. get_session_history(session_id):历史访问函数
def get_session_history(session_id: str):
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
- “历史获取器”(History Getter)。这是 LangChain 的一种约定格式(convention),是 RunnableWithMessageHistory 组件对接口的强制要求。
- 首次访问某个
session_id时,自动创建一个新的空历史对象;后续访问则返回已有对象。 - 关键点:该函数被
RunnableWithMessageHistory内部调用,用于加载/保存消息。
3. MessagesPlaceholder("history"):Prompt 中的记忆插入点
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个有记忆的 AI 助手..."),
MessagesPlaceholder(variable_name="history"), # ← 插入历史消息的位置
("human", "{input}")
])
MessagesPlaceholder是一个占位符,告诉 LangChain:“在这里插入会话历史”。- 它的名字
"history"必须与RunnableWithMessageHistory中的history_messages_key一致。
4. RunnableWithMessageHistory:包装器,绑定记忆的驱动
with_message_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="history",
)
- 这个包装器将普通链(
chain)升级为带记忆的可运行对象。 - 每次调用
.invoke()时,它会:- 根据
configurable.session_id找到对应的ChatMessageHistory; - 将历史消息自动注入到
prompt的"history"占位符; - 调用 LLM 得到响应;
- 自动将本次用户输入和 AI 回复追加到历史中(实现记忆持久化)。
- 根据
三、一次对话的执行流程
假设用户输入 "你好",session_id = "abc123":
-
调用入口:
response = with_message_history.invoke( {"input": "你好"}, config={"configurable": {"session_id": "abc123"}} ) -
加载历史:
get_session_history("abc123")被调用。- 若是首次,则
store["abc123"] = ChatMessageHistory()(空列表)。
-
构建完整 Prompt:
[System] 你是一个有记忆的 AI 助手... [Human] 你好(因为历史为空,
MessagesPlaceholder不插入任何内容) -
调用 LLM → 返回回复,例如
"你好!很高兴见到你。" -
自动保存对话:
- LangChain 自动将以下两条消息追加到
store["abc123"].messages:HumanMessage(content="你好")AIMessage(content="你好!很高兴见到你。")
- LangChain 自动将以下两条消息追加到
-
下次对话(如用户问“我刚才说了什么?”):
- 历史已包含上一轮对话;
- Prompt 变为:
[System] ... [Human] 你好 [AI] 你好!很高兴见到你。 [Human] 我刚才说了什么? - LLM 基于完整上下文作答:“你刚才说‘你好’。”
四、设计亮点与注意事项
✅ 优点
- 解耦清晰:记忆逻辑与业务逻辑分离,通过配置即可启用。
- 会话隔离:不同
session_id互不影响,适合 Web 应用(每个用户一个 ID)。 - 自动管理:无需手动 push/pop 消息,LangChain 自动维护历史。
⚠️ 注意事项
- 内存限制:
store存在内存中,长期运行可能 OOM。生产环境应替换为数据库持久化存储。 - Token 上限:历史消息会不断增长,可能超出模型上下文长度(如 Qwen-Max 为 32768 tokens)。需实现摘要压缩策略。
五、扩展方向
-
持久化存储
将store替换为 DB 后端。 -
记忆摘要(Memory Summarization)
当历史过长时,用另一个 LLM 调用生成摘要,替代原始消息。 -
多轮意图识别
结合记忆判断用户当前意图是否依赖历史(如订票场景中的“改签”)。
小结
这段代码展示了 LangChain 中“记忆型 Agent”的标准方法。
它不是魔法,而是通过显式管理对话历史 + 动态注入 Prompt 实现的工程技巧。
理解这一机制,是构建复杂对话系统(如客服机器人、个人助理)的重要基石。
记忆不是模型的天赋,而是我们赋予它的责任。
浙公网安备 33010602011771号