Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

4.2 短期记忆:对话历史管理

短期记忆是最基础的记忆形式——维护对话历史,让 Agent 知道"刚才我们说了什么"。

为什么这很重要?LLM 本身是无状态的——每次调用都是一次全新的请求,模型并不"记得"上一次对话的内容。如果用户在第一轮说"我叫张伟",第二轮问"我叫什么名字",模型如果不回传之前的对话历史,就会一脸茫然。短期记忆解决的正是这个问题:通过在每次请求中附带之前的对话记录,让模型"看起来"拥有了记忆。

但这带来一个新问题:LLM 的上下文窗口是有限的(GPT-4.1 约 128K Token)。随着对话越来越长,Token 消耗急剧增加,不仅费用高昂,还可能超出模型的处理上限。因此,我们需要对话历史管理策略来平衡"记忆完整性"和"资源效率"。

本节介绍三种策略:全量保留、滑动窗口、摘要压缩,它们适用于不同长度的对话场景。

短期记忆三种策略对比

为什么需要策略:Token 是硬约束

在深入代码之前,先理解问题的本质。对话每多一轮,所有"之前的消息 + 当前消息"都会重新发给 LLM 计算一次。Token 消耗随轮次线性增长,单次对话成本可能从几厘涨到几毛。

💡 粗略估算:一次 50 轮的技术支持对话,每轮平均 500 token,总输入 ≈ 50 × 500 = 25,000 token。即便用 gpt-4.1-mini,成本也在 0.07。如果是客服场景每天几千次对话,这笔费用必须严格控制。

三种策略就是为这个问题设计的"成本-记忆完整性"权衡点。

策略一:全量保留

最简单的实现。每一轮对话都加入历史数组,下次调用时全部发给模型。

from dataclasses import dataclass
from openai import OpenAI
import tiktoken

client = OpenAI()

@dataclass
class Message:
    role: str   # "user" / "assistant" / "system" / "tool"
    content: str
    token_count: int = 0

class ConversationHistory:
    """全量保留所有消息,token 计数用 tiktoken 精确统计。"""

    def __init__(self, system_prompt: str = "", model: str = "gpt-4.1"):
        self.encoding = tiktoken.encoding_for_model(model)
        self.messages: list[Message] = []
        if system_prompt:
            self.add_message("system", system_prompt)

    def add_message(self, role: str, content: str) -> None:
        tokens = len(self.encoding.encode(content))
        self.messages.append(Message(role, content, tokens))

    def total_tokens(self) -> int:
        return sum(m.token_count for m in self.messages)

    def chat(self, user_message: str) -> str:
        self.add_message("user", user_message)
        resp = client.chat.completions.create(
            model="gpt-4.1",
            messages=[{"role": m.role, "content": m.content} for m in self.messages]
        )
        reply = resp.choices[0].message.content
        self.add_message("assistant", reply)
        return reply

⚠️ 何时用:对话总轮次 < 10,或调试期临时使用。超过 10 轮就必须切到下面两种策略。

策略二:滑动窗口

核心思想:只保留最近 N 轮对话,丢弃更早的记录。类似于人类的短期记忆容量限制(心理学中"7±2"法则)。

class SlidingWindowMemory:
    """只保留最近 N 轮;system_prompt 始终保留。"""

    def __init__(self, system_prompt: str = "", max_turns: int = 10,
                 max_tokens: int = 8000, model: str = "gpt-4.1-mini"):
        self.system_prompt = system_prompt
        self.max_turns = max_turns
        self.max_tokens = max_tokens
        self.encoding = tiktoken.encoding_for_model(model)
        self.all_messages: list[dict] = []   # 完整历史(不传给 LLM)

    def _count(self, msgs: list[dict]) -> int:
        return sum(len(self.encoding.encode(m.get("content", ""))) for m in msgs)

    def _window(self) -> list[dict]:
        result = [{"role": "system", "content": self.system_prompt}] if self.system_prompt else []
        recent = self.all_messages[-(self.max_turns * 2):]
        # 双重保障:轮数 + token 数,任一超限就继续截
        while recent and self._count(result + recent) > self.max_tokens:
            recent = recent[2:]   # 每次移除最早一轮
        return result + recent

    def chat(self, user_message: str) -> str:
        self.all_messages.append({"role": "user", "content": user_message})
        window = self._window()
        resp = client.chat.completions.create(
            model="gpt-4.1-mini", messages=window
        )
        reply = resp.choices[0].message.content
        self.all_messages.append({"role": "assistant", "content": reply})
        return reply

为什么需要 max_turnsmax_tokens 双重保障?

  • 只用 max_turns:用户传了一段 5000 token 的代码,3 轮就可能撑爆上下文窗口
  • 只用 max_tokens:单轮 10 token 的闲聊,会塞 800 条消息,模型注意力反而分散

两者配合才能既控制成本,又保证每条消息有合理关注度。

⚠️ 滑动窗口的致命缺陷

窗口外的对话直接丢弃。如果用户在对话开头说"我是 Python 工程师,偏好简洁代码",等窗口滑过去后,Agent 就完全忘了这些身份信息。

解决方法:把用户身份、核心偏好等信息提取到长期记忆(4.3 节)。滑动窗口只负责"最近聊了什么"。

策略三:摘要压缩

核心思想:当历史超过阈值,用 LLM 把旧对话压缩成一段摘要,然后只保留"摘要 + 最近的消息"。

class SummaryMemory:
    """超阈值时自动用 LLM 压缩旧对话。"""

    def __init__(self, system_prompt: str = "", max_tokens: int = 3000,
                 model: str = "gpt-4.1-mini", summary_model: str = "gpt-4.1-mini"):
        self.system_prompt = system_prompt
        self.max_tokens = max_tokens
        self.encoding = tiktoken.encoding_for_model(model)
        self.summary: str = ""                          # 已压缩的历史
        self.recent: list[dict] = []                    # 未压缩的最近消息

    def _should_compress(self) -> bool:
        return sum(len(self.encoding.encode(m["content"]))
                   for m in self.recent) > self.max_tokens

    def _compress(self) -> str:
        """用 LLM 把 self.recent 压成 300 字内的摘要,并与已有摘要合并。"""
        convo = "\n".join(f"{m['role'].upper()}: {m['content']}"
                          for m in self.recent)
        prev = f"已有摘要:\n{self.summary}\n\n" if self.summary else ""
        prompt = f"""{prev}请将以下对话压缩为简洁摘要,保留用户需求、决策、偏好:
{convo}
要求:第三人称、客观简洁、不超过 300 字。"""
        resp = client.chat.completions.create(
            model="gpt-4.1-mini",
            messages=[{"role": "user", "content": prompt}],
            max_tokens=400
        )
        return resp.choices[0].message.content

    def chat(self, user_message: str) -> str:
        self.recent.append({"role": "user", "content": user_message})
        if self._should_compress():
            self.summary = self._compress()
            self.recent = [self.recent[-1]]              # 保留最后一条触发消息
        messages = []
        if self.system_prompt:
            messages.append({"role": "system", "content": self.system_prompt})
        if self.summary:
            messages.append({"role": "system",
                             "content": f"【历史摘要】\n{self.summary}"})
        messages.extend(self.recent)
        resp = client.chat.completions.create(model="gpt-4.1-mini", messages=messages)
        reply = resp.choices[0].message.content
        self.recent.append({"role": "assistant", "content": reply})
        return reply

摘要压缩的代价与适用场景

维度评价
优点保留早期关键信息(用户身份、决策、偏好);适合长对话
缺点每次压缩多一次 LLM 调用(成本和延迟增加);细节必然丢失
理想场景长技术支持对话(前半段大量排查,真正有用的是结论);项目复盘
不理想短对话、逻辑推理对话(细节缺失会破坏推理链)

💡 技巧:摘要 prompt 中明确要求"保留用户需求、决策、偏好",可以引导 LLM 留下真正重要的信息。也可以给"保留 5 条最重要的决策"这样的硬约束。

三种策略对比

策略适用场景优点缺点实现复杂度
全量保留< 10 轮的短对话信息完整、无丢失Token 线性增长,费用失控
滑动窗口一般客服 / 闲聊简单高效、延迟低窗口外信息全丢⭐⭐
摘要压缩长对话 / 项目复盘保留关键信息、Token 受控需额外 LLM 调用、细节丢失⭐⭐⭐

选型决策树

对话预计多长?
  ├─ < 10 轮 ──→ 全量保留
  ├─ 10~50 轮 ─→ 滑动窗口
  └─ > 50 轮 ──→ 滑动窗口 + 长期记忆(关键信息外提)
                  或 摘要压缩

实战中滑动窗口 + 长期记忆(4.3 节)的组合最常用——滑动窗口管"最近聊了什么",长期记忆管"用户身份和核心偏好"。


小结

策略一句话总结
全量保留简单但贵,只适合短对话
滑动窗口适合大部分场景,但会丢早期信息
摘要压缩适合长对话,代价是额外 LLM 调用

短期记忆的核心目标是让模型"看到"之前的对话。真正的难点不在于"记住什么",而在于"该记多少、该忘什么"。


下一节:4.3 长期记忆:向量数据库与检索