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 的 RunnableWithMessageHistoryChatMessageHistory 实现了这一能力,
其本质是:

在每次调用 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 教学中台系统,自动协调档案、教师、学生三方。

当学生提交问题时,平台自动完成以下流程:

  1. 识别学号(从请求中提取 session_id);
  2. 调用档案管理员get_session_history)取出该生档案;
  3. 填充教案模板:把历史对话塞进 MessagesPlaceholder("history")
  4. 调用 AI 教师模型 生成个性化回答;
  5. 更新档案:将本次问答追加到学生记录本,并存回档案库。

作用
全自动实现“加载记忆 → 生成回答 → 保存新记忆”的闭环,
是整个系统的“胶水”与引擎。


🔄 完整流程示例

学生(学号 S2026001)问:“我昨天问的那道二次函数题,能再讲一遍吗?”

  1. 教学平台收到请求,识别学号 S2026001
  2. 调用档案管理员,从学籍库取出该生档案(含昨日对话);
  3. 将历史记录插入教师教案的“学生历史栏”;
  4. 教师看到上下文,给出这次的回答;
  5. 平台把这次问答追加到学生档案,归档保存——记忆更新完成

📚 对照表:技术 ↔ 学籍管理

技术组件 学籍系统角色 核心职责
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() 时,它会:
    1. 根据 configurable.session_id 找到对应的 ChatMessageHistory
    2. 将历史消息自动注入到 prompt"history" 占位符;
    3. 调用 LLM 得到响应;
    4. 自动将本次用户输入和 AI 回复追加到历史中(实现记忆持久化)。

三、一次对话的执行流程

假设用户输入 "你好"session_id = "abc123"

  1. 调用入口

    response = with_message_history.invoke(
        {"input": "你好"},
        config={"configurable": {"session_id": "abc123"}}
    )
    
  2. 加载历史

    • get_session_history("abc123") 被调用。
    • 若是首次,则 store["abc123"] = ChatMessageHistory()(空列表)。
  3. 构建完整 Prompt

    [System] 你是一个有记忆的 AI 助手...
    [Human] 你好
    

    (因为历史为空,MessagesPlaceholder 不插入任何内容)

  4. 调用 LLM → 返回回复,例如 "你好!很高兴见到你。"

  5. 自动保存对话

    • LangChain 自动将以下两条消息追加到 store["abc123"].messages
      • HumanMessage(content="你好")
      • AIMessage(content="你好!很高兴见到你。")
  6. 下次对话(如用户问“我刚才说了什么?”):

    • 历史已包含上一轮对话;
    • Prompt 变为:
      [System] ...
      [Human] 你好
      [AI] 你好!很高兴见到你。
      [Human] 我刚才说了什么?
      
    • LLM 基于完整上下文作答:“你刚才说‘你好’。”

四、设计亮点与注意事项

✅ 优点

  • 解耦清晰:记忆逻辑与业务逻辑分离,通过配置即可启用。
  • 会话隔离:不同 session_id 互不影响,适合 Web 应用(每个用户一个 ID)。
  • 自动管理:无需手动 push/pop 消息,LangChain 自动维护历史。

⚠️ 注意事项

  • 内存限制store 存在内存中,长期运行可能 OOM。生产环境应替换为数据库持久化存储。
  • Token 上限:历史消息会不断增长,可能超出模型上下文长度(如 Qwen-Max 为 32768 tokens)。需实现摘要压缩策略。

五、扩展方向

  1. 持久化存储
    store 替换为 DB 后端。

  2. 记忆摘要(Memory Summarization)
    当历史过长时,用另一个 LLM 调用生成摘要,替代原始消息。

  3. 多轮意图识别
    结合记忆判断用户当前意图是否依赖历史(如订票场景中的“改签”)。


小结

这段代码展示了 LangChain 中“记忆型 Agent”的标准方法。
它不是魔法,而是通过显式管理对话历史 + 动态注入 Prompt 实现的工程技巧。
理解这一机制,是构建复杂对话系统(如客服机器人、个人助理)的重要基石。

记忆不是模型的天赋,而是我们赋予它的责任。

posted @ 2026-01-29 22:37  船山薪火  阅读(14)  评论(0)    收藏  举报
![image](https://img2024.cnblogs.com/blog/3174785/202601/3174785-20260125205854513-941832118.jpg)