告别“你是谁?”:手把手教你给大模型装上记忆大脑

你有没有遇到过这样的对话:

你问:“法国的首都是哪里?”
AI答:“巴黎。”
你再问:“那人口是多少?”
AI反问:“哪里的?”

这就是无记忆聊天机器人的典型症状。每一次消息都被当作全新对话,它不知道“那里”指的是巴黎。

真正的对话是这样工作的:上下文会延续,引用会累积,聊天机器人必须知道之前发生过什么。这篇文章就带你从零构建一个能记住对话历史的机器人——它会记得你两轮前说了啥,当前在聊什么主题,以及之前做出的决定。


你将从本文学到什么

  • 为什么大语言模型(LLM)天生无状态,以及如何“伪造”记忆
  • 对话历史模式:它到底是怎么工作的
  • 上下文窗口的限制以及为什么它重要
  • 滑动窗口记忆:只保留最后 N 条消息
  • 摘要记忆:压缩旧对话
  • 实体记忆:记住关于用户的具体事实
  • 用 LangChain 构建完整的多轮对话机器人
  • 跨会话持久化记忆

为什么 LLM 是无状态的

每次你调用 LLM 的 API,它都是从零开始。它对之前的调用完全没有记忆。它唯一能知道的上下文就是你放在当前 prompt 里的内容。

让聊天机器人工作的诀窍是:把整个对话历史都塞进每次的 prompt 里

第一轮:
  USER: “法国的首都是哪里?”
  → 发送给 LLM: "User: 法国的首都是哪里?"
  → LLM 回复: "巴黎"

第二轮:
  USER: “那人口是多少?”
  → 发送给 LLM:
      "User: 法国的首都是哪里?
       Assistant: 巴黎。
       User: 那人口是多少?"
  → LLM 看到完整上下文,知道“那”= 巴黎

第三轮:
  → 把第一轮、第二轮、以及第三轮的所有内容一起发过去

每条消息都会追加到一个不断增长的列表中。这个列表会被放进后续的每一次 prompt 里。LLM 可以回看,因为这些东西就在当前的上下文里。

很简单。但它有一个硬限制:上下文窗口


上下文窗口问题

每个 LLM 都有一个它能同时处理的最大 token 数。GPT-3.5-turbo:16k tokens。GPT-4:128k tokens。LLaMA-7B:4k tokens。

一段很长的对话会填满这个窗口。当对话超出限制时,你不能把一切都塞进去。你需要一个策略。

# 估计 token 数量(粗略:1 token ≈ 4 个英文字符)
def estimate_tokens(text: str) -> int:
    return len(text) // 4

def estimate_conversation_tokens(messages: list) -> int:
    total = 0
    for msg in messages:
        total += estimate_tokens(msg['content'])
        total += 4   # 每条消息的开销(角色、格式等)
    return total

# 演示对话如何快速填满窗口
messages = []
example_turns = [
    ("user", "给我讲讲机器学习。"),
    ("assistant", "机器学习是人工智能的一个领域,它让计算机能够从数据中学习,而无需显式编程。它包括监督学习(模型用标注样本训练)、无监督学习(不依赖标签发现模式)和强化学习(代理通过试错学习)。"),
    ("user", "那深度学习具体是什么?"),
    ("assistant", "深度学习是机器学习的一个子集,它使用多层神经网络。这些网络学习数据的分层表示,对图像、音频和文本任务尤其强大。2017 年提出的 Transformer 架构已成为现代深度学习系统的基础。"),
    ("user", "能举一些实际应用的例子吗?"),
    ("assistant", "当然!实际应用包括医学诊断中的图像分类、翻译和聊天机器人的自然语言处理、Netflix 和 Spotify 的推荐系统、银行欺诈检测,以及自动驾驶。深度学习通过大规模模式识别驱动了大多数这些应用。"),
]

print(f"{'轮次':<6} {'新增 tokens':<14} {'总 tokens':<14} {'占 4k 限制的百分比'}")
print("-" * 50)
for role, content in example_turns:
    messages.append({'role': role, 'content': content})
    total = estimate_conversation_tokens(messages)
    new   = estimate_tokens(content)
    print(f"{len(messages):<6} {new:<14} {total:<14} {total/4000:.1%}")

输出:

轮次    新增 tokens    总 tokens      占 4k 限制的百分比
--------------------------------------------------
1       12            16            0.4%
2       73            93            2.3%
3       13            110           2.8%
4       65            179           4.5%
5       15            198           5.0%
6       72            274           6.9%

一个关于复杂话题的长时间对话很容易达到 2000-3000 tokens。再加上 RAG 上下文和系统提示,很快就会触及限制。


策略 1:滑动窗口记忆

只保留最近 N 条消息。简单而有效。

from collections import deque
from typing import List, Optional

class SlidingWindowChatbot:
    """滑动窗口聊天机器人:只保留最近 N 条消息"""
    def __init__(self, model_pipeline, window_size: int = 10,
                 system_prompt: str = "你是一个乐于助人的助手。"):
        self.model         = model_pipeline
        self.window_size   = window_size  # 最多保留的消息条数
        self.system_prompt = system_prompt
        self.history       = deque(maxlen=window_size)  # 自动丢弃超出部分

    def chat(self, user_message: str) -> str:
        # 将用户消息添加到历史
        self.history.append({'role': 'user', 'content': user_message})

        # 构建带历史的 prompt
        messages = [
            {'role': 'system', 'content': self.system_prompt}
        ] + list(self.history)

        # 调用模型(演示中使用简单的文本格式)
        prompt = self._format_prompt(messages)
        response = self.model(prompt)

        # 将助手回复添加到历史
        self.history.append({'role': 'assistant', 'content': response})

        return response

    def _format_prompt(self, messages: List[dict]) -> str:
        formatted = ""
        for msg in messages:
            if msg['role'] == 'system':
                formatted += f"System: {msg['content']}\n\n"
            elif msg['role'] == 'user':
                formatted += f"Human: {msg['content']}\n"
            else:
                formatted += f"Assistant: {msg['content']}\n"
        formatted += "Assistant:"
        return formatted

    def get_history(self) -> list:
        return list(self.history)

    def clear(self):
        self.history.clear()
        print("对话历史已清空。")

# 模拟对话(用模拟模型演示)
def mock_model(prompt: str) -> str:
    """模拟的模型响应,实际使用请替换为真实 LLM 调用"""
    if "capital of france" in prompt.lower():
        return "法国的首都是巴黎。"
    elif "population" in prompt.lower() and "巴黎" in prompt:
        return "巴黎市常住人口约 210 万,大巴黎地区约 1200 万。"
    elif "famous landmark" in prompt.lower():
        return "巴黎以埃菲尔铁塔、卢浮宫、巴黎圣母院和凯旋门闻名。"
    elif "eiffel tower" in prompt.lower():
        return "埃菲尔铁塔建于 1887 年至 1889 年,由工程师古斯塔夫·埃菲尔设计,高 330 米。"
    else:
        return "我明白了。能告诉我更多吗?"

bot = SlidingWindowChatbot(mock_model, window_size=6)

# 模拟多轮对话
turns = [
    "法国的首都是哪里?",
    "那里的人口是多少?",
    "那个城市有哪些著名景点?",
    "跟我说说埃菲尔铁塔。",
    "它是什么时候建的?",
]

for user_input in turns:
    print(f"\n用户: {user_input}")
    response = bot.chat(user_input)
    print(f"机器人: {response}")

print(f"\n历史中有 {len(bot.get_history())} 条消息(最多 {bot.window_size})")

输出:

用户: 法国的首都是哪里?
机器人: 法国的首都是巴黎。

用户: 那里的人口是多少?
机器人: 巴黎市常住人口约 210 万,大巴黎地区约 1200 万。

用户: 那个城市有哪些著名景点?
机器人: 巴黎以埃菲尔铁塔、卢浮宫、巴黎圣母院和凯旋门闻名。

用户: 跟我说说埃菲尔铁塔。
机器人: 埃菲尔铁塔建于 1887 年至 1889 年,由工程师古斯塔夫·埃菲尔设计,高 330 米。

用户: 它是什么时候建的?
机器人: 我明白了。能告诉我更多吗?

历史中有 6 条消息(最多 6)

机器人通过上下文理解了“那里”(巴黎)和“那个城市”(巴黎)。滑动窗口保留了最后 6 条消息。


策略 2:摘要记忆

当历史变长时,将旧消息总结成摘要,只保留近期完整消息。

class SummaryMemoryChatbot:
    """摘要记忆聊天机器人:对旧对话进行自动总结,只保留近期完整消息"""
    def __init__(self, model_pipeline, summarizer_pipeline,
                 max_recent: int = 6, summary_threshold: int = 10,
                 system_prompt: str = "你是一个乐于助人的助手。"):
        self.model       = model_pipeline
        self.summarizer  = summarizer_pipeline
        self.max_recent  = max_recent  # 保留的最近完整消息数
        self.threshold   = summary_threshold  # 触发摘要的消息总数阈值
        self.system      = system_prompt
        self.history     = []
        self.summary     = ""  # 较早轮次的压缩记忆

    def _maybe_summarize(self):
        """如果历史达到阈值,触发摘要"""
        if len(self.history) < self.threshold:
            return

        # 摘要历史中最早的一半
        n_to_summarize = len(self.history) // 2
        old_messages   = self.history[:n_to_summarize]
        self.history   = self.history[n_to_summarize:]

        # 将旧消息格式化为文本
        old_text = "\n".join([
            f"{m['role'].title()}: {m['content']}"
            for m in old_messages
        ])

        # 调用摘要函数(生产环境中应调用 LLM)
        new_summary_input = f"{self.summary}\n\n{old_text}" if self.summary else old_text
        self.summary = self._summarize(new_summary_input)

        print(f"[记忆] 已将 {n_to_summarize} 条消息压缩为摘要")

    def _summarize(self, text: str) -> str:
        """模拟的摘要生成,实际应使用 LLM 或专门的摘要模型"""
        return "[早期对话摘要:用户询问了法国、巴黎、其人口(约 210 万)以及巴黎的地标,包括埃菲尔铁塔。]"

    def _format_prompt(self) -> str:
        parts = [f"System: {self.system}\n"]

        if self.summary:
            parts.append(f"[早期对话摘要]: {self.summary}\n")

        # 只保留最近 max_recent 条消息作为完整内容
        for msg in self.history[-self.max_recent:]:
            role = "Human" if msg['role'] == 'user' else "Assistant"
            parts.append(f"{role}: {msg['content']}")

        parts.append("Assistant:")
        return "\n".join(parts)

    def chat(self, user_message: str) -> str:
        self.history.append({'role': 'user', 'content': user_message})
        self._maybe_summarize()

        prompt   = self._format_prompt()
        response = self.model(prompt)
        self.history.append({'role': 'assistant', 'content': response})

        return response

    def memory_status(self):
        print(f"摘要: {'存在' if self.summary else '无'}")
        print(f"完整保留的最近消息数: {min(len(self.history), self.max_recent)}")
        print(f"总历史消息数: {len(self.history)}")

summary_bot = SummaryMemoryChatbot(mock_model, None, max_recent=6, summary_threshold=8)

for user_input in turns * 2:  # 重复对话两次以触发摘要
    response = summary_bot.chat(user_input)

summary_bot.memory_status()

策略 3:实体记忆

提取并存储关于用户或对话实体的具体事实。

import re
from typing import Dict

class EntityMemoryChatbot:
    """实体记忆聊天机器人:自动提取并记住用户的姓名、地点、兴趣等实体"""
    def __init__(self, model_pipeline,
                 system_prompt: str = "你是一个乐于助人的助手。"):
        self.model   = model_pipeline
        self.system  = system_prompt
        self.history = []
        self.entities: Dict[str, str] = {}  # 实体存储器,如 { 'name': '张三', 'topic': '机器学习' }

    def _extract_entities(self, message: str):
        """从用户消息中提取实体(简化版,生产环境应使用 NER 模型或 LLM)"""
        patterns = {
            'name':     r"(?:我叫|我是|我的名字是)\s+([\u4e00-\u9fa5]+)",  # 匹配中文姓名
            'location': r"(?:我住在|我在|我来自)\s+([\u4e00-\u9fa5]+(?:\s*[\u4e00-\u9fa5]+)?)",
            'job':      r"(?:我是|我是一名|我从事)\s+([\u4e00-\u9fa5]+(?:\s*[\u4e00-\u9fa5]+)?)",
            'topic':    r"(?:我想学|我在学|我需要帮助的方面是)\s+([\u4e00-\u9fa5\s]+)"
        }

        for entity_type, pattern in patterns.items():
            match = re.search(pattern, message)
            if match:
                self.entities[entity_type] = match.group(1).strip()

    def _build_entity_context(self) -> str:
        """构建包含已记住实体的上下文文本"""
        if not self.entities:
            return ""
        lines = ["关于用户的已知信息:"]
        for entity, value in self.entities.items():
            lines.append(f"  - {entity}: {value}")
        return "\n".join(lines)

    def _format_prompt(self) -> str:
        parts = [f"System: {self.system}"]

        entity_ctx = self._build_entity_context()
        if entity_ctx:
            parts.append(entity_ctx)

        for msg in self.history[-8:]:
            role = "Human" if msg['role'] == 'user' else "Assistant"
            parts.append(f"{role}: {msg['content']}")

        parts.append("Assistant:")
        return "\n".join(parts)

    def chat(self, user_message: str) -> str:
        self._extract_entities(user_message)
        self.history.append({'role': 'user', 'content': user_message})

        prompt   = self._format_prompt()
        response = self.model(prompt)
        self.history.append({'role': 'assistant', 'content': response})

        return response

# 测试实体记忆
def entity_mock_model(prompt: str) -> str:
    if "name" in prompt.lower() and "Alex" in prompt:
        return "很高兴认识你,Alex!"
    elif "Alex" in prompt and "推荐" in prompt:
        return "根据你对机器学习的兴趣,Alex,我建议从 Python 和 scikit-learn 开始。"
    elif "课程" in prompt:
        return "对于机器学习,Andrew Ng 的 Coursera 课程对初学者非常棒。"
    else:
        return "请告诉我你想学什么。"

entity_bot = EntityMemoryChatbot(entity_mock_model)

conversations = [
    "你好,我叫 Alex。",
    "我想学机器学习。",
    "你能推荐些什么吗?",
    "有什么课程吗?",
]

for user_input in conversations:
    print(f"\n用户: {user_input}")
    response = entity_bot.chat(user_input)
    print(f"机器人: {response}")

print(f"\n提取到的实体: {entity_bot.entities}")

输出:

用户: 你好,我叫 Alex。
机器人: 很高兴认识你,Alex!

用户: 我想学机器学习。
机器人: 请告诉我你想学什么。

用户: 你能推荐些什么吗?
机器人: 根据你对机器学习的兴趣,Alex,我建议从 Python 和 scikit-learn 开始。

用户: 有什么课程吗?
机器人: 对于机器学习,Andrew Ng 的 Coursera 课程对初学者非常棒。

提取到的实体: {'name': 'Alex', 'topic': '机器学习'}

机器人在所有轮次中记住了用户的名字和话题。


使用 OpenAI API 构建完整聊天机器人

import openai
import json
from datetime import datetime

class ProductionChatbot:
    """生产级聊天机器人,基于 OpenAI API,支持历史管理和持久化"""
    def __init__(
        self,
        system_prompt: str = "你是一个乐于助人的 AI 助手。",
        model: str = "gpt-3.5-turbo",
        max_history: int = 20,
        max_tokens: int = 500,
        temperature: float = 0.7
    ):
        self.client      = openai.OpenAI()
        self.model       = model
        self.max_history = max_history    # 最多保留多少条对话历史
        self.max_tokens  = max_tokens
        self.temperature = temperature
        self.history     = []
        self.system      = system_prompt
        self.created_at  = datetime.now()

    def chat(self, user_message: str) -> str:
        self.history.append({'role': 'user', 'content': user_message})

        # 若历史过长则裁剪
        if len(self.history) > self.max_history:
            self.history = self.history[-self.max_history:]

        # 构建发送给 API 的消息列表
        messages = [
            {'role': 'system', 'content': self.system}
        ] + self.history

        # 调用 OpenAI API
        response = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            max_tokens=self.max_tokens,
            temperature=self.temperature,
        )

        assistant_message = response.choices[0].message.content
        self.history.append({'role': 'assistant', 'content': assistant_message})

        return assistant_message

    def save_conversation(self, filepath: str):
        """将对话历史保存到文件"""
        data = {
            'created_at': self.created_at.isoformat(),
            'saved_at':   datetime.now().isoformat(),
            'model':      self.model,
            'system':     self.system,
            'messages':   self.history
        }
        with open(filepath, 'w') as f:
            json.dump(data, f, indent=2)
        print(f"已将 {len(self.history)} 条消息保存到 {filepath}")

    def load_conversation(self, filepath: str):
        """从文件加载对话历史"""
        with open(filepath, 'r') as f:
            data = json.load(f)
        self.history = data['messages']
        self.system  = data.get('system', self.system)
        print(f"已从 {filepath} 加载 {len(self.history)} 条消息")

    def reset(self):
        """重置对话"""
        self.history = []
        print("对话已重置。")

    def get_stats(self) -> dict:
        """获取对话统计信息"""
        n_user      = sum(1 for m in self.history if m['role'] == 'user')
        n_assistant = sum(1 for m in self.history if m['role'] == 'assistant')
        total_chars = sum(len(m['content']) for m in self.history)

        return {
            '轮次':            n_user,
            '总消息数':        len(self.history),
            '估计 token 数':   total_chars // 4,
            '历史深度':        len(self.history)
        }

# 使用示例
# bot = ProductionChatbot(
#     system_prompt="你是一个专注于实际例子的 ML 导师。",
#     model="gpt-3.5-turbo",
#     max_history=20
# )
# response = bot.chat("解释一下过拟合。")
# print(response)
# bot.save_conversation('session_001.json')
print("ProductionChatbot 已准备就绪(需要设置 OPENAI_API_KEY)")

LangChain 记忆:更轻松的方式

from langchain.memory import ConversationBufferMemory, ConversationSummaryMemory
from langchain.chains import ConversationChain
from langchain_community.llms import HuggingFacePipeline
from transformers import pipeline as hf_pipeline

# 创建 LLM
gen_pipe = hf_pipeline('text-generation', model='gpt2', max_new_tokens=100)
llm = HuggingFacePipeline(pipeline=gen_pipe)

# 缓冲区记忆:保留所有消息
buffer_memory = ConversationBufferMemory()

# 摘要记忆:自动在过长时进行摘要(需要 LLM 实例)
# summary_memory = ConversationSummaryMemory(llm=llm)

# 构建对话链
conversation = ConversationChain(
    llm=llm,
    memory=buffer_memory,
    verbose=False
)

# 开始聊天
result = conversation.predict(input="你好,我叫 Alex。")
print(f"机器人: {result[:100]}...")

result = conversation.predict(input="我叫什么名字?")
print(f"机器人: {result[:100]}...")

# 查看记忆内容
print(f"\n记忆缓冲区:\n{buffer_memory.buffer}")

跨会话持久化记忆

import json
import os

class PersistentChatbot:
    """持久化聊天机器人:将对话历史保存到磁盘,支持跨会话恢复"""
    def __init__(self, model_pipeline, session_id: str,
                 storage_dir: str = './chat_sessions',
                 max_history: int = 50):
        self.model       = model_pipeline
        self.session_id  = session_id
        self.storage_dir = storage_dir
        self.max_history = max_history
        self.history     = []
        self.metadata    = {}

        os.makedirs(storage_dir, exist_ok=True)
        self._load_session()  # 启动时自动加载历史

    def _session_path(self) -> str:
        return os.path.join(self.storage_dir, f"{self.session_id}.json")

    def _load_session(self):
        """从文件加载会话"""
        path = self._session_path()
        if os.path.exists(path):
            with open(path, 'r') as f:
                data = json.load(f)
            self.history  = data.get('history', [])
            self.metadata = data.get('metadata', {})
            print(f"已加载会话 '{self.session_id}',包含 {len(self.history)} 条消息")
        else:
            print(f"新会话 '{self.session_id}' 已创建")

    def _save_session(self):
        """将会话保存到文件"""
        data = {
            'session_id':   self.session_id,
            'last_updated': datetime.now().isoformat(),
            'history':      self.history,
            'metadata':     self.metadata
        }
        with open(self._session_path(), 'w') as f:
            json.dump(data, f, indent=2)

    def chat(self, user_message: str) -> str:
        self.history.append({'role': 'user', 'content': user_message})

        if len(self.history) > self.max_history:
            self.history = self.history[-self.max_history:]

        response = self.model(self._format_prompt())
        self.history.append({'role': 'assistant', 'content': response})
        self._save_session()  # 每次聊天后自动保存

        return response

    def _format_prompt(self) -> str:
        parts = []
        for msg in self.history[-10:]:
            role = "Human" if msg['role'] == 'user' else "Assistant"
            parts.append(f"{role}: {msg['content']}")
        parts.append("Assistant:")
        return "\n".join(parts)

    def list_sessions(self) -> list:
        """列出存储目录中的所有会话 ID"""
        sessions = []
        for f in os.listdir(self.storage_dir):
            if f.endswith('.json'):
                sessions.append(f.replace('.json', ''))
        return sessions

# 使用示例
persistent_bot = PersistentChatbot(mock_model, session_id='user_alex_001')
persistent_bot.chat("法国的首都是哪里?")
persistent_bot.chat("那里的人口是多少?")

print(f"\n已保存的会话: {persistent_bot.list_sessions()}")
print(f"历史消息数: {len(persistent_bot.history)} 条")

聊天机器人质量检查清单

checklist = {
    "记忆管理": [
        "机器人能记住 5 轮之前的上下文吗?",
        "它能正确处理指代关系吗?(如“那里”、“它”、“他们”)",
        "它会重复用户已经提供的信息吗?"
    ],
    "上下文窗口": [
        "它能在超长对话中不崩溃吗?",
        "当历史过长时,有没有优雅的回退方案?",
        "摘要后的信息是否准确,没有丢失关键内容?"
    ],
    "对话质量": [
        "能保持对话始终围绕主题吗?",
        "能正确引用之前的决策吗?",
        "能优雅处理话题切换吗?"
    ],
    "持久化": [
        "能保存对话供以后使用吗?",
        "能从之前的会话中恢复继续吗?",
        "存储格式是否可读且易于调试?"
    ],
    "边缘情况": [
        "用户询问不在记忆中的内容时怎么办?",
        "用户自己矛盾怎么办?",
        "用户消息非常短或非常长时能处理吗?"
    ]
}

for category, items in checklist.items():
    print(f"\n{category}:")
    for item in items:
        print(f"  [ ] {item}")

快速速查表

记忆类型与适用场景

  • 缓冲区(全部历史):适合短对话。保留所有消息,每次都全部传入。
  • 滑动窗口:适合中等长度对话。只保留最近 N 条消息。
  • 摘要记忆:适合长对话。将旧消息总结成摘要,保留近期完整消息。
  • 实体记忆:适合需要记住用户具体信息的场景。自动提取并存储命名实体。
  • 持久化记忆:适合多会话聊天机器人。从磁盘或数据库保存/加载记录。

常见操作模式与代码

  • 添加历史:history.append({'role': 'user', 'content': msg})
  • 裁剪历史:history = history[-max_size:]
  • 构建消息:[{'role': 'system', 'content': system}] + history
  • 保存会话:json.dump({'history': history}, f)
  • 加载会话:history = json.load(f)['history']
  • LangChain 缓冲区记忆:ConversationBufferMemory()
  • LangChain 摘要记忆:ConversationSummaryMemory(llm=llm)

练习挑战

Level 1:
用本地 GPT-2 构建一个 SlidingWindowChatbot。进行 10 轮关于你感兴趣话题的对话。结束后打印完整历史,验证机器人能正确引用前几轮的内容。

Level 2:
实现 SummaryMemoryChatbot 并用真实的摘要模型(如 T5-small)进行总结。每 8 轮对话后,将前半部分用模型总结。测试 20 轮对话,打印每次摘要触发的时间点,检查摘要是否准确。

Level 3:
构建 PersistentChatbot,将会话保存到磁盘。先开启一个对话,关闭程序,重新启动,加载会话,继续对话。验证机器人记得上一轮说了什么。添加一个 /history 命令,打印之前所有会话的简短摘要。


参考文献


最终篇,第 100 篇:OpenAI API: Build With GPT-4。API 设置、聊天补全、函数调用、流式输出和成本管理。系列最后一篇将把所有内容收束在一起。

直达网址:https://python.langchain.com/docs/modules/memory/

类似文章