Agent 记忆系统#

为什么 Agent 需要记忆?#

大模型本身是无状态的:每次调用都从零开始,不记得上一轮说了什么。 Agent 需要记忆才能处理长期任务、维持上下文连贯性。

记忆的四个层次#

层次

类型

适用场景

容量

1

对话历史(滑动窗口)

短期对话

最近 N 条

2

摘要记忆

长期对话

压缩后的摘要

3

实体记忆

人物/事实跟踪

结构化键值对

4

向量记忆

语义检索

全量历史

本章将逐一实现这四种记忆,最后组合使用。

import json
import os
from collections import deque
import litellm
litellm.drop_params = True
from dotenv import load_dotenv

load_dotenv()

MODEL = os.getenv("LLM_MODEL")
EMBED_MODEL = os.getenv("EMBED_MODEL", "openai/text-embedding-3-small")

print(f"对话模型:{MODEL}")
print(f"嵌入模型:{EMBED_MODEL}")
# gpt-5/o系列不支持自定义temperature值,统一用安全wrapper
def _c(**kw):
    _m = kw.get('model', MODEL)
    if any(_m.startswith(p) for p in ('openai/gpt-5','openai/o1','openai/o3','openai/o4')):
        kw.pop('temperature', None)
    return litellm.completion(**kw)
对话模型:openai/gpt-5-mini
嵌入模型:openai/text-embedding-3-small

Section 1:对话历史(滑动窗口)#

最简单的记忆方式:保留最近的 N 条消息。

优点:实现简单,延迟低 缺点:窗口之外的信息完全丢失

使用 collections.dequemaxlen 参数,自动丢弃最旧的消息。

class ConversationMemory:
    """
    滑动窗口对话记忆。
    只保留最近 window_size 条消息,超出时自动丢弃最旧的。
    """

    def __init__(self, window_size: int = 6, system_prompt: str = "你是一个友好的助手。"):
        self.window_size = window_size
        self.system_prompt = system_prompt
        # deque maxlen 会自动丢弃超出部分(先进先出)
        self._history = deque(maxlen=window_size)
        self._total_turns = 0  # 统计总对话轮数

    def chat(self, user_message: str) -> str:
        """发送消息并获取回复,自动维护滑动窗口"""
        self._history.append({"role": "user", "content": user_message})
        self._total_turns += 1

        messages = [
            {"role": "system", "content": self.system_prompt},
            *list(self._history),
        ]

        response = _c(
            model=MODEL,
            messages=messages,
            temperature=0.7,
            max_tokens=256,
        )
        reply = response.choices[0].message.content
        self._history.append({"role": "assistant", "content": reply})

        return reply

    @property
    def current_window_size(self) -> int:
        return len(self._history)

    def show_stats(self):
        print(f"总对话轮数:{self._total_turns}")
        print(f"窗口大小:{self.window_size} 条(当前保留 {self.current_window_size} 条)")
        print(f"丢弃轮次:{max(0, self._total_turns * 2 - self.window_size)} 条消息")


# ── 演示:超长对话,观察窗口截断 ────────────────────────────
memory = ConversationMemory(window_size=4)  # 只保留最近 4 条消息

conversations = [
    "我叫小明,今年 25 岁",
    "我喜欢打篮球",
    "我最近在学 Python",
    "我有一只猫叫小白",
    "我的名字是什么?",  # 测试:能否记住第 1 条消息中的名字?
]

print("滑动窗口记忆演示(窗口大小=4)")
print("="*50)

for i, msg in enumerate(conversations):
    print(f"\n[第 {i+1} 轮] 用户:{msg}")
    reply = memory.chat(msg)
    print(f"        助手:{reply}")
    print(f"        当前窗口:{memory.current_window_size} 条消息")

print()
memory.show_stats()
滑动窗口记忆演示(窗口大小=4)
==================================================

[第 1 轮] 用户:我叫小明,今年 25 岁
        助手:
        当前窗口:2 条消息

[第 2 轮] 用户:我喜欢打篮球
        助手:
        当前窗口:4 条消息

[第 3 轮] 用户:我最近在学 Python
        助手:
        当前窗口:4 条消息

[第 4 轮] 用户:我有一只猫叫小白
        助手:
        当前窗口:4 条消息

[第 5 轮] 用户:我的名字是什么?
        助手:
        当前窗口:4 条消息

总对话轮数:5
窗口大小:4 条(当前保留 4 条)
丢弃轮次:6 条消息

观察上面的输出:当问「我的名字是什么?」时,模型很可能不记得了。 因为包含名字的消息已经被滑出了窗口。

这就是摘要记忆要解决的问题。

Section 2:摘要记忆#

核心思路:当历史消息超过阈值时,调用 LLM 将旧消息压缩成摘要, 只保留摘要 + 最近 N 条消息。

旧消息 (1~n)  →  LLM 摘要  →  [摘要] + 最近 k 条

压缩率通常能达到 5:1 甚至更高,大幅降低 token 消耗。

class SummaryMemory:
    """
    摘要记忆:超过阈值后自动压缩旧消息为摘要。

    工作流程:
    1. 正常累积对话历史
    2. 当历史超过 compress_threshold 条时,触发压缩
    3. 将旧的 (总量 - keep_recent) 条消息发给 LLM 生成摘要
    4. 用摘要替换旧消息,保留最近 keep_recent 条
    """

    def __init__(
        self,
        system_prompt: str = "你是一个友好的助手。",
        compress_threshold: int = 8,  # 超过这个数量触发压缩
        keep_recent: int = 4,          # 压缩后保留最近 N 条
    ):
        self.system_prompt = system_prompt
        self.compress_threshold = compress_threshold
        self.keep_recent = keep_recent
        self._history = []       # 完整当前历史
        self._summary = ""       # 压缩后的摘要
        self._compression_count = 0
        self._original_tokens = 0
        self._summary_tokens = 0

    def _compress(self):
        """将旧消息压缩为摘要"""
        # 要压缩的消息(保留最近 keep_recent 条)
        to_compress = self._history[:-self.keep_recent] if self.keep_recent > 0 else self._history[:]
        recent = self._history[-self.keep_recent:] if self.keep_recent > 0 else []

        # 估算原始 token 数(简单用字符数 / 2 近似)
        old_text = "\n".join(f"{m['role']}: {m['content']}" for m in to_compress)
        self._original_tokens += len(old_text) // 2

        # 构建压缩提示
        compress_prompt = f"""请将以下对话历史压缩为简洁的摘要,保留所有重要信息(人名、事实、决定等)。

已有摘要:{self._summary or '(无)'}

新增对话:
{old_text}

请输出更新后的摘要(100 字以内):"""

        response = _c(
            model=MODEL,
            messages=[{"role": "user", "content": compress_prompt}],
            temperature=0,
            max_tokens=200,
        )
        self._summary = response.choices[0].message.content
        self._summary_tokens += len(self._summary) // 2
        self._history = recent
        self._compression_count += 1

        print(f"  [摘要记忆] 第 {self._compression_count} 次压缩完成")
        print(f"  摘要:{self._summary[:80]}..." if len(self._summary) > 80 else f"  摘要:{self._summary}")

    def _build_messages(self) -> list:
        """构建发给模型的消息列表"""
        system = self.system_prompt
        if self._summary:
            system += f"\n\n对话摘要(之前的对话记录):\n{self._summary}"
        return [{"role": "system", "content": system}, *self._history]

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

        # 检查是否需要压缩
        if len(self._history) >= self.compress_threshold:
            self._compress()

        response = _c(
            model=MODEL,
            messages=self._build_messages(),
            temperature=0.7,
            max_tokens=256,
        )
        reply = response.choices[0].message.content
        self._history.append({"role": "assistant", "content": reply})
        return reply

    def show_compression_stats(self):
        ratio = self._original_tokens / max(self._summary_tokens, 1)
        print(f"压缩次数:{self._compression_count}")
        print(f"原始 token(估算):{self._original_tokens}")
        print(f"摘要 token(估算):{self._summary_tokens}")
        print(f"压缩率:约 {ratio:.1f}:1")


# ── 演示:长对话摘要压缩 ──────────────────────────────────
smem = SummaryMemory(compress_threshold=6, keep_recent=2)

long_conversation = [
    "你好,我叫张伟,是一名软件工程师",
    "我正在做一个 AI 聊天机器人项目",
    "项目使用 Python 和 FastAPI",
    "我的团队有 5 个人",
    "我们预计下个月上线",
    "我最喜欢的编程语言是 Rust",
    "我住在北京朝阳区",
    "我叫什么名字?我在做什么项目?",  # 测试记忆
]

print("摘要记忆演示(压缩阈值=6,保留最近=2)")
print("="*50)

for i, msg in enumerate(long_conversation):
    print(f"\n[第 {i+1} 轮] 用户:{msg}")
    reply = smem.chat(msg)
    print(f"        助手:{reply}")

print()
smem.show_compression_stats()
摘要记忆演示(压缩阈值=6,保留最近=2)
==================================================

[第 1 轮] 用户:你好,我叫张伟,是一名软件工程师
        助手:你好,张伟!很高兴认识你。你是软件工程师——太棒了。我要怎么帮助你?例如:

- 代码问题(调试、性能优化、重构)
- 技术选型或架构设计
- 面试准备(算法题、系统设计、行为面试)
- 简历或岗位申请建议
- 学习路线或资源推荐(前端/后端/云/机器学习等)
- 项目点子或具体实现方案

告诉我你主要使用的编程语言/技术栈以及当前最想解决的事情,我就能给出更具体的建议或示例。

[第 2 轮] 用户:我正在做一个 AI 聊天机器人项目
        助手:

[第 3 轮] 用户:项目使用 Python 和 FastAPI
        助手:

[第 4 轮] 用户:我的团队有 5 个人
  [摘要记忆] 第 1 次压缩完成
  摘要:
        助手:

[第 5 轮] 用户:我们预计下个月上线
        助手:

[第 6 轮] 用户:我最喜欢的编程语言是 Rust
  [摘要记忆] 第 2 次压缩完成
  摘要:
        助手:

[第 7 轮] 用户:我住在北京朝阳区
        助手:

[第 8 轮] 用户:我叫什么名字?我在做什么项目?
  [摘要记忆] 第 3 次压缩完成
  摘要:
        助手:

压缩次数:3
原始 token(估算):202
摘要 token(估算):0
压缩率:约 202.0:1

Section 3:实体记忆#

实体记忆专门追踪对话中提到的关键实体:人名、地名、事实、偏好等。

工作流程:

  1. 每轮对话后,调用 LLM 从新消息中提取实体

  2. 更新实体字典(新信息覆盖或补充旧信息)

  3. 将相关实体注入系统提示,让模型「记住」它们

优势:精确跟踪用户信息,不会因窗口截断而遗忘关键事实

class EntityMemory:
    """
    实体记忆:提取并存储对话中的关键实体信息。

    实体以 JSON 字典形式存储,例如:
    {
        "用户名字": "小明",
        "用户的猫": "叫小白",
        "用户职业": "工程师"
    }
    """

    def __init__(self, system_prompt: str = "你是一个友好的助手。"):
        self.system_prompt = system_prompt
        self._history = []       # 最近几条消息(小窗口)
        self._entities = {}      # 实体字典
        self._window = 6        # 保留最近 6 条

    def _extract_entities(self, new_messages: list) -> dict:
        """从新消息中提取实体,返回新发现的实体字典"""
        text = "\n".join(f"{m['role']}: {m['content']}" for m in new_messages)
        existing = json.dumps(self._entities, ensure_ascii=False)

        prompt = f"""从以下对话中提取关键实体信息(人名、地点、宠物、偏好、事实等)。
已知实体:{existing}

新对话:
{text}

请以 JSON 格式输出新发现或更新的实体(只输出 JSON,无需其他文字):
例如:{{"用户名字": "小明", "用户的猫": "叫小白"}}
如果没有新实体,输出:{{}}"""

        response = _c(
            model=MODEL,
            messages=[{"role": "user", "content": prompt}],
            temperature=0,
            max_tokens=200,
        )
        content = response.choices[0].message.content.strip()

        # 尝试解析 JSON
        try:
            # 去掉可能的 Markdown 代码块
            if content.startswith("```"):
                content = content.split("\n", 1)[1].rsplit("\n", 1)[0]
            return json.loads(content)
        except json.JSONDecodeError:
            return {}

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

        # 构建包含实体信息的系统提示
        system = self.system_prompt
        if self._entities:
            entity_str = "\n".join(f"- {k}{v}" for k, v in self._entities.items())
            system += f"\n\n已知用户信息:\n{entity_str}"

        messages = [
            {"role": "system", "content": system},
            *self._history[-self._window:],
        ]

        response = _c(
            model=MODEL,
            messages=messages,
            temperature=0.7,
            max_tokens=256,
        )
        reply = response.choices[0].message.content
        self._history.append({"role": "assistant", "content": reply})

        # 异步提取实体(实际应用中可以放后台线程)
        new_entities = self._extract_entities(self._history[-2:])
        if new_entities:
            self._entities.update(new_entities)
            print(f"  [实体记忆] 更新实体:{new_entities}")

        return reply

    def show_entities(self):
        print("当前已知实体:")
        for k, v in self._entities.items():
            print(f"  {k}: {v}")


# ── 演示:跨轮次实体记忆 ─────────────────────────────────
emem = EntityMemory()

entity_conversation = [
    "我叫李雷,是一名医生",
    "我有一只猫叫小白,它非常喜欢吃鱼",
    "我住在上海",
    "今天天气真好",  # 无关消息
    "我的宠物喜欢吃什么?",  # 需要记忆:小白喜欢吃鱼
    "我是做什么工作的?",    # 需要记忆:医生
]

print("实体记忆演示")
print("="*50)

for i, msg in enumerate(entity_conversation):
    print(f"\n[第 {i+1} 轮] 用户:{msg}")
    reply = emem.chat(msg)
    print(f"        助手:{reply}")

print()
emem.show_entities()
实体记忆演示
==================================================

[第 1 轮] 用户:我叫李华,是一名医生
        助手:

[第 2 轮] 用户:我有一只猫叫小白,它非常喜欢吃鱼
        助手:

[第 3 轮] 用户:我住在上海
  [实体记忆] 更新实体:{'居住地': '上海'}
        助手:

[第 4 轮] 用户:今天天气真好
        助手:

[第 5 轮] 用户:我的宠物喜欢吃什么?
        助手:

[第 6 轮] 用户:我是做什么工作的?
        助手:

当前已知实体:
  居住地: 上海

Section 4:向量记忆(轻量实现)#

向量记忆将历史对话转换为嵌入向量,通过语义相似度检索最相关的历史片段。

不需要 ChromaDB 等向量数据库——用 NumPy 实现余弦相似度即可。

适用场景:历史记录很长,但当前问题只与部分历史相关。

import numpy as np


def get_embedding(text: str) -> np.ndarray:
    """获取文本的嵌入向量"""
    response = litellm.embedding(
        model=EMBED_MODEL,
        input=[text],
    )
    return np.array(response.data[0]["embedding"])


def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
    """计算两个向量的余弦相似度(-1 到 1,越大越相似)"""
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8))


class VectorMemory:
    """
    向量记忆:基于语义相似度检索相关历史对话。

    每条历史记录存储:
    - text: 原始文本
    - embedding: 嵌入向量
    - role: 角色 (user/assistant)
    """

    def __init__(
        self,
        system_prompt: str = "你是一个智能助手。",
        top_k: int = 3,              # 检索最相关的 K 条历史
        similarity_threshold: float = 0.5,  # 相似度阈值
    ):
        self.system_prompt = system_prompt
        self.top_k = top_k
        self.similarity_threshold = similarity_threshold
        self._store = []   # [{"text": ..., "embedding": ..., "role": ...}]

    def _store_turn(self, role: str, text: str):
        """将一条消息存入向量存储"""
        embedding = get_embedding(text)
        self._store.append({
            "role": role,
            "text": text,
            "embedding": embedding,
        })

    def _retrieve_relevant(self, query: str) -> list:
        """根据查询语义检索最相关的历史条目"""
        if not self._store:
            return []

        query_emb = get_embedding(query)
        scored = [
            (cosine_similarity(query_emb, item["embedding"]), item)
            for item in self._store
        ]
        # 过滤低于阈值,按相似度排序
        scored = [(s, item) for s, item in scored if s >= self.similarity_threshold]
        scored.sort(key=lambda x: x[0], reverse=True)
        return [item for _, item in scored[:self.top_k]]

    def chat(self, user_message: str) -> str:
        # 检索相关历史
        relevant = self._retrieve_relevant(user_message)

        # 构建消息
        system = self.system_prompt
        if relevant:
            context = "\n".join(
                f"[历史] {item['role']}: {item['text']}" for item in relevant
            )
            system += f"\n\n相关历史对话:\n{context}"
            print(f"  [向量记忆] 检索到 {len(relevant)} 条相关历史")

        messages = [
            {"role": "system", "content": system},
            {"role": "user", "content": user_message},
        ]

        response = _c(
            model=MODEL,
            messages=messages,
            temperature=0.7,
            max_tokens=256,
        )
        reply = response.choices[0].message.content

        # 存储本轮对话
        self._store_turn("user", user_message)
        self._store_turn("assistant", reply)

        return reply

    def show_store_size(self):
        print(f"向量存储条目数:{len(self._store)}")
        if self._store:
            dim = len(self._store[0]['embedding'])
            print(f"嵌入维度:{dim}")


# ── 演示:语义检索历史 ─────────────────────────────────────
vmem = VectorMemory(top_k=2, similarity_threshold=0.4)

# 预先存入一些历史对话
seed_history = [
    ("user", "我喜欢打羽毛球,每周打三次"),
    ("assistant", "羽毛球是很好的有氧运动!"),
    ("user", "我的猫叫花花,是一只橘猫"),
    ("assistant", "橘猫很可爱!花花听起来很讨人喜欢。"),
    ("user", "我在学习机器学习,最近在研究 Transformer"),
    ("assistant", "Transformer 是现代 NLP 的基础架构。"),
]

print("存入历史对话到向量记忆...")
for role, text in seed_history:
    vmem._store_turn(role, text)
print(f"已存入 {len(vmem._store)} 条历史")

# 现在测试语义检索
print("\n向量记忆语义检索演示")
print("="*50)

test_questions = [
    "我平时有什么运动爱好?",  # 应检索到羽毛球相关
    "我的宠物叫什么名字?",    # 应检索到花花相关
]

for q in test_questions:
    print(f"\n用户:{q}")
    reply = vmem.chat(q)
    print(f"助手:{reply}")

vmem.show_store_size()
存入历史对话到向量记忆...
已存入 6 条历史

向量记忆语义检索演示
==================================================

用户:我平时有什么运动爱好?
  [向量记忆] 检索到 2 条相关历史
助手:你平时喜欢打羽毛球,大约每周打三次。要不要我帮你制定训练计划或给些练习建议?

用户:我的宠物叫什么名字?
  [向量记忆] 检索到 2 条相关历史
助手:你的宠物叫花花,是一只橘猫。
向量存储条目数:10
嵌入维度:1536

Section 5:组合记忆(HybridMemory)#

实际应用中,单一记忆类型往往不够。 HybridMemory 将滑动窗口和实体记忆结合:

  • 滑动窗口:保证最近对话的连贯性

  • 实体记忆:跨越窗口持续追踪关键信息

演示场景:一个关于项目管理的多轮对话。

class HybridMemory:
    """
    混合记忆:滑动窗口 + 实体记忆。

    - 短期:滑动窗口保证最近对话连贯
    - 长期:实体记忆跨越窗口保持关键信息
    """

    def __init__(
        self,
        system_prompt: str = "你是一个专业的项目管理助手。",
        window_size: int = 6,
    ):
        self.system_prompt = system_prompt
        self._window = deque(maxlen=window_size)
        self._entities = {}
        self._turn_count = 0

    def _update_entities(self, user_msg: str, assistant_msg: str):
        """从最新一轮对话中提取实体"""
        prompt = f"""从以下对话中提取关键信息(项目名称、截止日期、负责人、状态、决定等)。
已知信息:{json.dumps(self._entities, ensure_ascii=False)}

新对话:
用户:{user_msg}
助手:{assistant_msg}

以 JSON 输出新发现的信息,没有则输出 {{}}:"""

        try:
            response = _c(
                model=MODEL,
                messages=[{"role": "user", "content": prompt}],
                temperature=0,
                max_tokens=150,
            )
            content = response.choices[0].message.content.strip()
            if content.startswith("```"):
                content = content.split("\n", 1)[1].rsplit("\n", 1)[0]
            new_entities = json.loads(content)
            if new_entities:
                self._entities.update(new_entities)
                print(f"  [实体更新] {new_entities}")
        except Exception:
            pass  # 解析失败时静默跳过

    def _build_system_prompt(self) -> str:
        system = self.system_prompt
        if self._entities:
            info = "\n".join(f"- {k}{v}" for k, v in self._entities.items())
            system += f"\n\n当前已知项目信息:\n{info}"
        return system

    def chat(self, user_message: str) -> str:
        self._turn_count += 1
        self._window.append({"role": "user", "content": user_message})

        messages = [
            {"role": "system", "content": self._build_system_prompt()},
            *list(self._window),
        ]

        response = _c(
            model=MODEL,
            messages=messages,
            temperature=0.7,
            max_tokens=256,
        )
        reply = response.choices[0].message.content
        self._window.append({"role": "assistant", "content": reply})

        # 每轮更新实体
        self._update_entities(user_message, reply)
        return reply

    def status(self):
        print(f"对话轮次:{self._turn_count}")
        print(f"窗口消息数:{len(self._window)}")
        print(f"实体数量:{len(self._entities)}")
        if self._entities:
            print("已知实体:")
            for k, v in self._entities.items():
                print(f"  {k}: {v}")


# ── 演示:项目管理多轮对话 ───────────────────────────────
hmem = HybridMemory(system_prompt="你是一个专业的项目管理助手,帮助用户追踪项目进度。")

project_conversation = [
    "我们有个项目叫'星辰计划',负责人是王芳",
    "项目截止日期是 3 月 31 日",
    "目前完成了需求分析和设计阶段",
    "开发团队遇到了一个技术难题:数据库性能问题",
    "我们决定改用 PostgreSQL 替换 MySQL",
    "好的,现在说说今天的天气",  # 无关消息
    "回到项目:星辰计划现在进展如何?负责人是谁?截止日期是什么时候?",
]

print("混合记忆演示:项目管理对话")
print("="*50)

for i, msg in enumerate(project_conversation):
    print(f"\n[第 {i+1} 轮] 用户:{msg}")
    reply = hmem.chat(msg)
    print(f"        助手:{reply}")

print("\n" + "="*50)
print("记忆系统状态:")
hmem.status()
混合记忆演示:项目管理对话
==================================================

[第 1 轮] 用户:我们有个项目叫'星辰计划',负责人是王芳
        助手:

[第 2 轮] 用户:项目截止日期是 3 月 31 日
        助手:

[第 3 轮] 用户:目前完成了需求分析和设计阶段
        助手:

[第 4 轮] 用户:开发团队遇到了一个技术难题:数据库性能问题
        助手:

[第 5 轮] 用户:我们决定改用 PostgreSQL 替换 MySQL
        助手:

[第 6 轮] 用户:好的,现在说说今天的天气
        助手:

[第 7 轮] 用户:回到项目:星辰计划现在进展如何?负责人是谁?截止日期是什么时候?
        助手:

==================================================
记忆系统状态:
对话轮次:7
窗口消息数:6
实体数量:0

进阶:记忆持久化#

实际部署中,Agent 重启后需要恢复记忆。 下面展示如何将实体记忆序列化到 JSON 文件。

import json
from pathlib import Path


def save_memory(entities: dict, filepath: str = "/tmp/agent_memory.json"):
    """将实体记忆保存到 JSON 文件"""
    with open(filepath, "w", encoding="utf-8") as f:
        json.dump(entities, f, ensure_ascii=False, indent=2)
    print(f"记忆已保存到:{filepath}")


def load_memory(filepath: str = "/tmp/agent_memory.json") -> dict:
    """从 JSON 文件加载实体记忆"""
    path = Path(filepath)
    if not path.exists():
        print("没有找到持久化记忆文件,从空白开始")
        return {}
    with open(filepath, encoding="utf-8") as f:
        entities = json.load(f)
    print(f"从 {filepath} 加载了 {len(entities)} 条实体记忆")
    return entities


# 保存 HybridMemory 的实体到文件
save_memory(hmem._entities)

# 模拟重启后加载
loaded = load_memory()
print("\n加载的实体信息:")
for k, v in loaded.items():
    print(f"  {k}: {v}")
记忆已保存到:/tmp/agent_memory.json
从 /tmp/agent_memory.json 加载了 0 条实体记忆

加载的实体信息:

练习题#

  1. 改进滑动窗口:当前按条数截断,试着改为按 token 数截断(提示:litellm.token_counter())。

  2. 摘要质量评估:实现一个函数,对比摘要前后的信息保留率(让 LLM 判断摘要是否遗漏了关键信息)。

  3. 实体冲突处理:如果用户说「我的猫叫小白」,后来又说「我没有猫」,实体记忆应该如何处理矛盾?修改 EntityMemory._extract_entities 来支持实体删除。

  4. 向量记忆优化:当前每次都对所有历史计算相似度(O(n) 复杂度)。研究如何用 FAISS 或 ScaNN 加速检索。

总结#

四种记忆类型对比#

类型

实现复杂度

容量

适用场景

主要缺点

滑动窗口

简单

最近 N 条

短期对话、问答

窗口外信息丢失

摘要记忆

中等

压缩历史 + N 条

长对话、客服

摘要可能丢细节

实体记忆

中等

无限(键值对)

用户画像、CRM

只记结构化信息

向量记忆

较高

全量历史

知识库问答

需要嵌入模型

选择建议#

  • 客服机器人:摘要记忆 + 实体记忆

  • 个人助手:滑动窗口 + 实体记忆(HybridMemory)

  • 知识库 QA:向量记忆

  • 生产环境:配合 Redis/PostgreSQL 做持久化存储

下一章我们将学习多 Agent 协作,让多个专门的 Agent 分工合作完成复杂任务。

# 知识检验:内存类型速查
summary_table = {
    "ConversationMemory": {
        "核心机制": "deque(maxlen=N)",
        "适用场景": "短期对话",
        "LLM 调用次数": "每轮 1 次",
    },
    "SummaryMemory": {
        "核心机制": "超阈值后调用 LLM 摘要",
        "适用场景": "长对话",
        "LLM 调用次数": "正常 1 次 + 压缩时额外 1 次",
    },
    "EntityMemory": {
        "核心机制": "每轮提取实体注入 system prompt",
        "适用场景": "用户画像跟踪",
        "LLM 调用次数": "每轮 2 次(回答 + 提取)",
    },
    "VectorMemory": {
        "核心机制": "余弦相似度检索相关历史",
        "适用场景": "大量历史中语义检索",
        "LLM 调用次数": "每轮 1 次 + 嵌入调用",
    },
}

print("记忆系统速查表")
print("="*60)
for name, info in summary_table.items():
    print(f"\n{name}")
    for k, v in info.items():
        print(f"  {k}: {v}")
记忆系统速查表
============================================================

ConversationMemory
  核心机制: deque(maxlen=N)
  适用场景: 短期对话
  LLM 调用次数: 每轮 1 次

SummaryMemory
  核心机制: 超阈值后调用 LLM 摘要
  适用场景: 长对话
  LLM 调用次数: 正常 1 次 + 压缩时额外 1 次

EntityMemory
  核心机制: 每轮提取实体注入 system prompt
  适用场景: 用户画像跟踪
  LLM 调用次数: 每轮 2 次(回答 + 提取)

VectorMemory
  核心机制: 余弦相似度检索相关历史
  适用场景: 大量历史中语义检索
  LLM 调用次数: 每轮 1 次 + 嵌入调用