如何给 Agent 打造“最强大脑“?深度解析短期记忆与长期记忆的分层设计

在这里插入图片描述

同一个 Agent,单轮对话对答如流,第二天却"失忆";任务一长就开始胡说八道;记忆越攒越多,最后新旧事实打架。这些往往不是模型不够强,而是你没给它的"记忆系统"分层。本文用可复现的 Python 代码,讲透短期记忆的上下文管理、长期记忆的读写链路、向量记忆与 Markdown 记忆的取舍,以及让记忆持续进化的"代谢机制"。

前言

在 AI Agent 的开发中,有一个非常普遍的痛点:Agent 在单次对话中表现出色,但一旦任务变长、或跨越多个会话,它就会"健忘"或"逻辑断层"。

  • 你昨天告诉它"我用的是 Java 21,偏好关注性能而非代码规范",今天它又开始给你挑命名规范、还默认你在写 Java 8;
  • 一个需要多步推进的长任务,跑到中段它就忘了最初的目标,开始绕圈子;
  • 聊得越久,它越容易把几小时前的工具调用结果和现在的问题混为一谈,给出自相矛盾的答案。

这些现象的根源,往往不在模型,而在 记忆(Memory)

业内有个被广泛引用的 Agent 核心公式:

Agent = LLM + Planning + Memory + Tools
  • LLM 是推理引擎(大脑算力);
  • Planning 决定它如何拆解任务、规划步骤;
  • Tools 决定它能调用什么能力;
  • Memory 决定它记得什么、学到了什么、此刻在处理什么

很多人把精力都花在换更强的模型和调 Prompt 上,却忽略了:记忆系统直接决定了 Agent 能否从"聊天机器人"进化为真正的"数字员工"。 一个会遗忘、会混淆、不会积累经验的 Agent,能力再强也只能活在"这一句话"里。

读完本文你将收获:

  • 理解短期记忆与长期记忆的双层架构(物理与逻辑解耦);
  • 掌握短期记忆的上下文管理手段:滑动窗口、摘要压缩、重型结果卸载;
  • 学会长期记忆的读写链路:对话后语义提纯写入、会话前向量检索注入;
  • 厘清向量记忆 vs Markdown 记忆的适用边界;
  • 拿到一套让记忆持续进化的**“代谢机制”**(反思、合并、遗忘)与 L1/L2/L3 进阶路径,全部配可复现代码。

背景或问题:Agent 为什么会"健忘"?

先抛三个几乎每个 Agent 开发者都踩过的坑:

  1. 跨会话失忆:用户关掉对话再打开,Agent 完全不记得你是谁、你的偏好是什么,每次都从零开始。
  2. 长对话后期"变笨":聊到几十轮以后,Agent 开始忘掉前面说过的话、格式混乱、甚至"幻觉式"编造已经不存在的上下文。
  3. 记忆越攒越乱:好不容易给 Agent 加了记忆,结果它把"用户技术栈是 Java 8"和后来"升级到 Java 21"两条都记着,下次回答时新旧事实打架,输出更乱了。

这三个问题的共同点是:它们都不是模型能力问题,而是记忆工程问题。 解决它们,需要先把记忆"分层"。


核心思路:记忆的双层架构——物理与逻辑的彻底解耦

一个成熟的 Agent 记忆系统,必须在物理和逻辑上划分为两层:短期记忆长期记忆

1. 短期记忆(Short-term Memory):Session 级的"工作空间"

短期记忆是 Agent 在当前单次会话中持有的暂存信息,主要依托于 LLM 自身的上下文窗口(Context Window)。它负责记录这一轮任务里必须保留的信息:用户刚提的问题、Planning 的中间计划、工具调用的入参与返回(Observations)等。

工程约束:尽管主流模型窗口已经达到 1M Token,但有两个硬约束不能忽视:

  • 推理成本随长度线性增长:上下文越长,单次推理越贵、越慢;
  • "中间丢失(Lost in the Middle)"现象:模型对上下文首尾的注意力强,对中段的信息容易"视而不见"——窗口再大,不代表它真的都用得上。

管理手段(按轻重排列):

  • 滑动窗口(Sliding Window):只保留最近 N 轮对话,丢弃最早的消息,简单粗暴但会丢上下文;
  • 摘要压缩(Summarization):把被丢弃的旧消息用 LLM 压缩成一段摘要,用信息损耗换取空间;
  • 重型结果卸载(Offloading):把大文件、长返回结果存到外部存储(数据库 / 文件),Prompt 里只保留一个引用(如"结果见 result_8842.json"),需要时再取回。

2. 长期记忆(Long-term Memory):跨 Session 的"经验沉淀"

长期记忆是活在会话之外的持久化知识库,负责沉淀用户偏好、历史决策和过往教训。

核心特征——高度个性化:这一点经常被混淆。长期记忆 ≠ RAG。 RAG 挂载的通常是"共享知识源"(如公司规章、产品文档、公开资料),对所有用户一致;而长期记忆管理的是 Agent 与特定用户交互中动态沉淀的私有背景(“这个用户更在意性能”“这个项目禁止直连生产库”)。一个是"公共图书馆",一个是"私人笔记本"。

读写链路

  • 写入:通常发生在对话结束后,通过 LLM 对短期记忆进行"语义提纯"——从一堆闲聊和工具结果里,提取出值得长期记住的"事实/偏好/教训";
  • 读取:通常发生在新会话开始时,通过向量检索把与当前问题相关的历史经验,注入到上下文里。

一句话总结:短期记忆是"工作台",长期记忆是"笔记本"。 工作台要随时清空保持清爽,笔记本要持续沉淀且能被检索召回。两者物理隔离、逻辑解耦,Agent 才能既专注当下,又不忘过去。


实现步骤

下面把这套记忆系统拆成可落地的工程模块,每个都配可复现的代码。为方便演示,向量与 embedding 部分用纯 Python + NumPy 做了轻量实现(实际工程替换为真实 embedding 模型和向量库即可),所有示例都能直接跑起来。

步骤一:短期记忆——给上下文窗口装一个"自动整理器"

短期记忆的关键,是在 token 预算内保住最相关的信息。我们实现一个 ShortTermMemory:它维护一个消息列表,当累计 token 超过预算时,自动触发摘要压缩——把较早的消息压成一段摘要,只保留最近几轮原文。

# short_term_memory.py
# 短期记忆:滑动窗口 + token 预算 + 自动摘要压缩
from dataclasses import dataclass


@dataclass
class Message:
    role: str        # "user" / "assistant" / "tool"
    content: str


def _approx_tokens(text: str) -> int:
    """近似 token 计数(中文约 1 字 ≈ 1 token)。
    生产环境建议用 tiktoken 等精确分词器。"""
    return max(1, len(text))


class ShortTermMemory:
    def __init__(self, token_budget: int = 6000, keep_recent: int = 4):
        self.token_budget = token_budget  # 上下文 token 预算(生产环境通常数千)
        self.keep_recent = keep_recent    # 压缩时保留最近几条原文
        self.messages: list[Message] = []
        self.summary: str = ""            # 历史摘要(被压缩掉的旧消息)

    def add(self, role: str, content: str) -> None:
        self.messages.append(Message(role, content))
        self._maybe_compress()

    def _used_tokens(self) -> int:
        """当前上下文已用 token(摘要 + 全部消息)。"""
        total = _approx_tokens(self.summary)
        for m in self.messages:
            total += _approx_tokens(m.content)
        return total

    def _summarize(self, old_messages: list[Message]) -> str:
        """模拟 LLM 的有损摘要:每条旧消息只保留要点。
        生产环境应调用大模型,输出比原文更短的总结。"""
        bits = [f"[{m.role}]{m.content[:6]}" for m in old_messages]
        return "; ".join(bits)

    def _maybe_compress(self) -> None:
        """超过预算时,把较早的消息压缩进摘要。
        用 while 循环:一次压缩可能仍超预算,需反复压缩直到达标或仅剩最近几条。"""
        guard = 0  # 防御性兜底,避免极端情况下死循环
        while (self._used_tokens() > self.token_budget
               and len(self.messages) > self.keep_recent
               and guard < 20):
            guard += 1
            # 要压缩的 = 全部消息 - 需要保留的最近几条
            n_to_compress = max(1, len(self.messages) - self.keep_recent)
            to_compress = self.messages[:n_to_compress]
            self.messages = self.messages[n_to_compress:]

            new_summary = self._summarize(to_compress)
            self.summary = ((self.summary + " | " + new_summary).strip(" |")
                            if self.summary else new_summary)
            print(f"[短期记忆] 触发摘要压缩,{n_to_compress} 条旧消息已并入摘要,"
                  f"当前上下文 {self._used_tokens()} tokens")

    def to_prompt(self) -> str:
        """拼装成可塞进 Prompt 的上下文。"""
        parts = []
        if self.summary:
            parts.append(f"[历史摘要] {self.summary}")
        for m in self.messages:
            parts.append(f"[{m.role}] {m.content}")
        return "\n".join(parts)


if __name__ == "__main__":
    # 小预算方便演示触发压缩(生产环境 token_budget 通常为数千)
    mem = ShortTermMemory(token_budget=80, keep_recent=3)
    mem.add("user", "我是后端工程师,主力语言是 Java 21")
    mem.add("assistant", "好的,已记录你的技术栈。")
    mem.add("user", "帮我审查这段代码的性能问题,关注 OOM 风险")
    mem.add("tool", "内存报告已卸载:见 result_8842.json")  # 重型结果卸载,只留引用
    mem.add("user", "那这部分为什么会有内存泄漏?")
    print("\n=== 最终进入 Prompt 的上下文 ===")
    print(mem.to_prompt())

运行结果:

[短期记忆] 触发摘要压缩,1 条旧消息已并入摘要,当前上下文 73 tokens
[短期记忆] 触发摘要压缩,1 条旧消息已并入摘要,当前上下文 95 tokens

=== 最终进入 Prompt 的上下文 ===
[历史摘要] [user]我是后端工程 | [assistant]好的,已记录
[user] 帮我审查这段代码的性能问题,关注 OOM 风险
[tool] 内存报告已卸载:见 result_8842.json
[user] 那这部分为什么会有内存泄漏?

可以看到:较早的闲聊被有损压进了 [历史摘要](每条只留要点),最近的关键对话保留原文;而那条 2000 行的内存报告走了重型结果卸载——Prompt 里只留一个引用 result_8842.json,需要时再取回。整体 token 始终被压在预算内。_maybe_compresswhile 循环而非单次 if,是因为一次压缩后如果仍超预算,要继续压缩更早的消息。

避坑提示:摘要压缩是有损的。关键约束(如"禁止直连生产库")建议从短期记忆中卸载到长期记忆或 AGENTS.md,而不是依赖随时可能被压缩掉的对话历史。能用"引用 + 按需取回"解决的(如大文件),就别让它一直占着上下文。

步骤二:长期记忆——打通"写入"与"读取"两条链路

短期记忆解决的是"单次会话内的专注",长期记忆解决的是"跨会话的连续性"。它有两条核心链路:写入(语义提纯)读取(检索注入)
在这里插入图片描述
下面是一个可运行的长期记忆实现(向量部分用 NumPy 简化,便于理解链路;生产环境把 fake_embed 换成真实 embedding、把内存列表换成向量库即可):

# long_term_memory.py
# 长期记忆:写入(语义提纯) + 读取(向量检索注入)
import hashlib
import time
from dataclasses import dataclass, field
import numpy as np


def fake_embed(text: str, dim: int = 64) -> np.ndarray:
    """文本哈希生成确定性伪向量,仅供跑通链路。
    生产环境替换为真实 embedding 模型(如 text-embedding-3-small)。"""
    h = hashlib.md5(text.encode("utf-8")).digest()
    vec = np.frombuffer((h * ((dim // 16) + 1))[: dim * 4], dtype=np.uint8)
    return (vec.astype(np.float32) - 128) / 128.0


def cosine(a: np.ndarray, b: np.ndarray) -> float:
    return float(np.dot(a, b) / ((np.linalg.norm(a) * np.linalg.norm(b)) + 1e-8))


@dataclass
class MemoryItem:
    content: str                 # 提纯后的事实/偏好/教训
    category: str                # fact / preference / lesson
    embedding: np.ndarray
    created_at: float = field(default_factory=time.time)


class LongTermMemory:
    def __init__(self):
        self.store: list[MemoryItem] = []

    # ---------- 写入链路:语义提纯 ----------
    def write_from_session(self, short_term_text: str) -> None:
        """会话结束后调用:从短期记忆里'语义提纯'出值得长期记住的内容。
        实际工程用 LLM 抽取,这里给一段可直接复用的提取 Prompt 模板。"""
        extract_prompt = f"""请从下面这段对话记录中,提取出值得长期记住的条目。
只提取稳定的事实、用户偏好或可复用的教训,忽略闲聊和一次性信息。
每条输出一行,格式:分类|内容。分类只能是 fact/preference/lesson。

对话记录:
{short_term_text}

示例输出:
preference|该用户更关注性能而非代码规范
fact|该用户主力技术栈是 Java 21
lesson|审查该用户代码时应优先关注 OOM 风险
"""
        # 演示:用写死的提取结果代替 LLM 调用
        extracted = [
            ("preference", "该用户更关注性能而非代码规范"),
            ("fact", "该用户主力技术栈是 Java 21"),
            ("lesson", "审查该用户代码时应优先关注 OOM 风险"),
        ]
        for category, content in extracted:
            self.store.append(
                MemoryItem(content=content, category=category,
                           embedding=fake_embed(content))
            )
        print(f"[长期记忆·写入] 语义提纯出 {len(extracted)} 条经验并持久化")

    # ---------- 读取链路:向量检索注入 ----------
    def retrieve(self, query: str, top_k: int = 3) -> list[MemoryItem]:
        """新会话开始时调用:按当前问题检索最相关的历史经验。"""
        if not self.store:
            return []
        q_emb = fake_embed(query)
        ranked = sorted(self.store, key=lambda m: cosine(q_emb, m.embedding),
                        reverse=True)
        return ranked[:top_k]

    def build_context(self, query: str) -> str:
        """把检索到的长期记忆拼成一段,注入 Agent 上下文。"""
        hits = self.retrieve(query)
        if not hits:
            return ""
        lines = [f"- [{m.category}] {m.content}" for m in hits]
        return "[相关长期记忆]\n" + "\n".join(lines)


if __name__ == "__main__":
    ltm = LongTermMemory()

    # 场景一:上一轮会话结束后,把短期记忆提纯写入长期记忆
    print("=== 会话1结束:写入长期记忆 ===")
    ltm.write_from_session("用户说他用 Java 21,审查代码更在意性能 ...")

    # 场景二:新一轮会话开始,带着用户新问题检索注入
    print("\n=== 会话2开始:检索注入 ===")
    ctx = ltm.build_context("帮我看看这段代码有没有性能问题")
    print(ctx)

运行结果:

=== 会话1结束:写入长期记忆 ===
[长期记忆·写入] 语义提纯出 3 条经验并持久化

=== 会话2开始:检索注入 ===
[相关长期记忆]
- [lesson] 审查该用户代码时应优先关注 OOM 风险
- [fact] 该用户主力技术栈是 Java 21
- [preference] 该用户更关注性能而非代码规范

这就是 Agent"第二天还记得你"的底层链路:会话结束做语义提纯写入,会话开始做检索注入。 注意 write_from_session 里给出的提取 Prompt 模板是可直接复用的——生产中把它接到你的 LLM 调用上即可。

步骤三:向量记忆 vs Markdown 记忆——别选错工具

实现长期记忆时,开发者常在"重型向量库"和"轻量 Markdown"之间犹豫。其实它们不是二选一,而是适用边界不同

维度向量记忆(Vector Memory)Markdown 记忆(如 CLAUDE.md / AGENTS.md)
本质海量非结构化文本的语义检索可审计、可版本控制的"入职手册"
适合存多样化历史记录、长周期行为偏好项目规范、固定汇报偏好、团队协作约定
优势能从数十万条历史里找相似片段透明可审计(随时看到 Agent 记了什么)、原生 Git 版本控制、零运维成本
代价运维复杂,易产生"虚假关联"(语义相近但业务无关)不擅长语义模糊检索,条目多了需要人工维护
更新方式自动提纯写入 + 向量召回人工或 Agent 按规则追加,错误驱动进化

1. 向量记忆:海量非结构化文本的语义检索

当需要从数十万条交互历史中寻找相似片段时,向量检索不可替代。典型场景:跨数月分析某个用户的行为偏好、从大量历史工单里找相似案例。

代价是运维复杂度高,且有个隐蔽的坑——“虚假关联”:两段话语义相近,但业务上毫无关系,向量检索却把它们排到前面,反而误导 Agent。所以向量记忆一定要配合重排(rerank) 和业务过滤一起用。

2. Markdown 记忆:可审计的"入职手册"

这是一种极其务实的方案:把规则、约定和踩坑记录写进仓库里的 Markdown 文件(如 CLAUDE.mdAGENTS.md)。它的优势是 透明可审计(随时能看到 Agent 到底"记"了什么)、版本控制(原生集成 Git,记忆变更可追溯)、零运维成本

实践建议遵循 "错误驱动进化"原则:只有当 Agent 真的犯过某个错,才值得在 AGENTS.md 里增加一条防错规则——而不是一开始就写一份几千字的"完美手册"。下面是一个错误驱动进化的极简管理器:

# markdown_memory.py
# Markdown 记忆:错误驱动进化,按"踩坑"增量沉淀防错规则
from pathlib import Path


class MarkdownMemory:
    """维护一个 AGENTS.md 风格的硬性约束文件。
    核心原则:只在 Agent 真的犯错后,才追加一条防错规则。"""

    def __init__(self, path: str = "AGENTS.md"):
        self.path = Path(path)

    def learn_from_mistake(self, mistake: str, rule: str) -> None:
        """Agent 犯错后调用:把教训沉淀为一条可机械化检查的规则。"""
        section = "\n## 防错规则(错误驱动进化)\n" if "防错规则" not in self._read() else ""
        entry = f"\n- [触发场景] {mistake}\n  [约束规则] {rule}\n"
        with self.path.open("a", encoding="utf-8") as f:
            f.write(section + entry)
        print(f"[Markdown记忆] 已从错误中学习并沉淀规则:{rule}")

    def _read(self) -> str:
        return self.path.read_text(encoding="utf-8") if self.path.exists() else ""

    def to_prompt(self) -> str:
        """把硬性约束注入 Agent 上下文(通常放在 System Prompt 里)。"""
        return self._read()


if __name__ == "__main__":
    mm = MarkdownMemory(path="AGENTS.md")
    # Agent 刚才在 handler 里裸写了 SQL,被结构测试拦下了
    mm.learn_from_mistake(
        mistake="Agent 在 src/handlers/user.py 中直接调用了 cursor.execute",
        rule="handler 层禁止裸写 SQL,数据库访问必须走 repository 层",
    )

运行后,AGENTS.md 会增量长出一条防错规则,下次 Agent 启动时把它注入 System Prompt,就不会再犯同样的错。

选型心法共享的、硬性的、需要全团队对齐的约束 → Markdown 记忆;私有的、动态的、海量的用户历史 → 向量记忆。 两者通常并存:AGENTS.md 管"规矩",向量库管"经验"。

步骤四:记忆的"代谢机制"——反思、合并与遗忘

这是本文最关键的部分。如果系统只是无脑追加(Append) 信息,长期记忆很快会变成一堆高噪声的流水账——重复条目堆积、过时信息干扰、新旧事实打架。生产级系统必须有一套"代谢机制"。
在这里插入图片描述

1. 自我反思(Self-Reflection)

任务完成后,Agent 启动一个异步任务复盘成败,把教训提取为更高层的 Meta-Knowledge(元知识)

例如:“在处理该用户的 Java 代码审查时,他更在意性能而非规范,未来应优先关注 OOM 风险。” 这种从具体事件中抽象出来的经验,比原始对话记录有用得多。

2. 记忆聚类与合并(Consolidation)

当出现大量碎片化、重复记录时(比如用户在 10 次对话里都提到同一个项目),系统自动触发合并任务,把它们整理成一条完整的"实体百科"。这能减少向量库冗余、提升检索一致性,避免同一个事实被召回 5 个几乎一样的版本。

3. 权重衰减与遗忘(Forgetting)

记忆不是越多越好,过时信息会严重干扰判断

  • 时间衰减:模仿人类遗忘曲线,为每条记忆维护一个分数,随时间推移降低权重——太久没被用上的记忆,逐渐"淡出";
  • 冲突解决:当新旧事实冲突时(如用户技术栈从 Java 8 升级到 Java 21),旧记忆应被标记为废弃或软删除,而不是和新记忆并存打架。

下面把"代谢机制"实现成可运行的代码:

# memory_metabolism.py
# 记忆代谢机制:自我反思 + 聚类合并 + 权重衰减与遗忘
import math
import time
from dataclasses import dataclass


@dataclass
class MemRecord:
    content: str
    score: float = 1.0          # 当前权重/新鲜度得分
    created_at: float = 0.0     # 创建时间戳
    last_used: float = 0.0      # 最近一次被检索使用的时间
    status: str = "active"      # active / deprecated(软删除)


class MemoryMetabolism:
    def __init__(self, half_life_days: float = 30.0):
        self.half_life_seconds = half_life_days * 24 * 3600
        self.records: list[MemRecord] = []

    # ---------- 1. 自我反思:从一次任务中提取元知识 ----------
    def self_reflect(self, task_summary: str) -> str:
        """任务完成后异步调用:把具体事件抽象为可复用的元知识。
        实际工程用 LLM 生成,这里给 Prompt 模板 + 演示结果。"""
        prompt = f"""请基于以下任务复盘,提炼一条可复用的'元知识':
要求抽象到'下次遇到类似情况该怎么办'的层面,而不是复述流水账。

任务复盘:{task_summary}

输出格式:一条元知识句子。
"""
        # 演示结果(生产环境替换为 LLM 调用)
        meta = "该用户在代码审查中更在意性能而非规范,未来优先关注 OOM 与性能风险"
        self.records.append(MemRecord(
            content=meta, created_at=time.time(), last_used=time.time()
        ))
        print(f"[代谢·反思] 提取元知识:{meta}")
        return meta

    # ---------- 2. 聚类合并:碎片化重复记录整理成一条 ----------
    def consolidate(self, similarity_fn) -> None:
        """把内容高度相似的碎片记录合并成一条。
        similarity_fn(a, b) -> bool:由业务定义什么叫'同一件事'。"""
        merged_any = False
        i = 0
        while i < len(self.records):
            group = [self.records[i]]
            rest = []
            for r in self.records[i + 1:]:
                if r.status == "active" and similarity_fn(group[0].content, r.content):
                    group.append(r)
                else:
                    rest.append(r)
            if len(group) > 1:
                # 合并:取并集语义,旧的软删除
                consolidated = MemRecord(
                    content=" | ".join(g.content for g in group),
                    score=max(g.score for g in group),
                    created_at=min(g.created_at for g in group),
                    last_used=max(g.last_used for g in group),
                )
                for g in group:
                    g.status = "deprecated"  # 软删除碎片
                self.records = self.records[:i] + [consolidated] + rest
                merged_any = True
                print(f"[代谢·合并] 合并 {len(group)} 条碎片为 1 条")
            i += 1
        if not merged_any:
            print("[代谢·合并] 暂无需合并的碎片")

    # ---------- 3. 权重衰减与遗忘 ----------
    def decay_and_forget(self, now: float, conflict_fn=None) -> None:
        """时间衰减降权 + 新旧冲突时软删除旧记忆。"""
        for r in self.records:
            if r.status != "active":
                continue
            age = now - r.last_used
            # 半衰期模型:每过一个 half_life,得分减半
            r.score = math.pow(0.5, age / self.half_life_seconds)
            # 得分低于阈值 = 长期未被使用,软删除
            if r.score < 0.1:
                r.status = "deprecated"
                print(f"[代谢·遗忘] 长期未使用,软删除:{r.content[:24]}...")

        # 冲突解决:新旧事实打架时,废弃旧的
        if conflict_fn:
            active = [r for r in self.records if r.status == "active"]
            for i, a in enumerate(active):
                for b in active[i + 1:]:
                    if conflict_fn(a.content, b.content):
                        older, newer = (a, b) if a.created_at <= b.created_at else (b, a)
                        older.status = "deprecated"
                        print(f"[代谢·冲突] 检测到冲突,废弃旧记忆:{older.content[:24]}...")

    def active_records(self) -> list[MemRecord]:
        return [r for r in self.records if r.status == "active"]


def _same_entity(a: str, b: str) -> bool:
    """演示用:两条记忆都提到'订单系统'才算同一实体的重复记录。
    生产中用聚类 / LLM 判断,注意不要把'Java 8'和'Java 21'这种冲突事实误判为重复。"""
    return "订单系统" in a and "订单系统" in b


def _conflict(a: str, b: str) -> bool:
    """演示用:新旧技术栈冲突。生产中用 LLM 判断事实是否矛盾。"""
    return ("Java 8" in a and "Java 21" in b) or ("Java 8" in b and "Java 21" in a)


if __name__ == "__main__":
    now = time.time()
    meta = MemoryMetabolism(half_life_days=30)

    # 1) 自我反思:沉淀元知识(刚发生,最"新鲜")
    meta.self_reflect("用户要求审查 Java 代码,重点关注 OOM")
    # 模拟一些历史记录,注意 created_at / last_used 各不相同
    meta.records += [
        MemRecord(content="用户在做的项目是订单系统", created_at=now - 86400 * 10),
        MemRecord(content="用户的项目里有个订单系统模块", created_at=now - 86400 * 5),
        MemRecord(content="用户技术栈是 Java 8", created_at=now - 86400 * 90),
        MemRecord(content="用户技术栈升级为 Java 21", created_at=now - 86400 * 2),
    ]
    for r in meta.records:
        r.last_used = r.created_at

    # 2) 聚类合并:把"订单系统"两条碎片合成一条
    print()
    meta.consolidate(_same_entity)

    # 3) 权重衰减与遗忘(now 取当前时刻,演示半衰期降权 + 新旧冲突解决)
    print()
    meta.decay_and_forget(now=now, conflict_fn=_conflict)

    print("\n=== 最终存活的有效记忆 ===")
    for r in meta.active_records():
        print(f"  [score={r.score:.2f}] {r.content}")

运行结果:

[代谢·反思] 提取元知识:该用户在代码审查中更在意性能而非规范,未来优先关注 OOM 与性能风险

[代谢·合并] 合并 2 条碎片为 1 条

[代谢·冲突] 检测到冲突,废弃旧记忆:用户技术栈是 Java 8...

=== 最终存活的有效记忆 ===
  [score=1.00] 该用户在代码审查中更在意性能而非规范,未来优先关注 OOM 与性能风险
  [score=0.89] 用户在做的项目是订单系统 | 用户的项目里有个订单系统模块
  [score=0.95] 用户技术栈升级为 Java 21

可以看到代谢机制的威力:碎片化的"订单系统"被合并成一条;过时且冲突的"Java 8"被软删除,只留下最新的"Java 21";而刚提取的元知识因为最"新鲜",拿到了接近满分的权重——记忆库从"高噪声流水账"变成了"干净的事实表"。注意 _same_entity 故意只匹配"订单系统",没有把"Java 8 / Java 21"误判为重复——否则两条矛盾的事实会被错误合并。

关键提醒decay_and_forget 里的半衰期(默认 30 天)和遗忘阈值(0.1)需要按业务调。对"用户偏好"这类稳定事实,半衰期应设长一些;对"临时任务状态"这类易过期的,应设短一些。冲突解决务必保留"新"事实——除非有证据表明新事实是错的。


完整方案:L1/L2/L3 记忆系统进阶路径

打造 Agent 的"大脑"不能一蹴而就,建议按下面三个层级循序渐进。每一层都是上一层的前提,跳级往往事倍功半。

层级名称做什么解决的问题本文对应
L1基础管好 Session 内的 Context,做摘要压缩和 Token 预算长对话后期变笨、Lost in the MiddleShortTermMemory
L2规范建立项目级 CLAUDE.md / AGENTS.md,沉淀硬性约束和 SOPAgent 反复犯同类错误、团队不对齐MarkdownMemory
L3经验引入向量库存储个性化偏好,建立离线"反思-合并-遗忘"闭环跨会话失忆、记忆越攒越乱LongTermMemory + MemoryMetabolism

落地建议:

  1. 先 L1,把上下文管住:在没做好单会话上下文管理前,谈长期记忆都是空中楼阁。先用 ShortTermMemory 守住 token 预算;
  2. 再 L2,把规矩焊死:每犯一个错,就往 AGENTS.md 增量沉淀一条防错规则,让 Agent"入职"越来越熟练;
  3. 最后 L3,让经验流动:当 L1/L2 稳定后,再上向量库 + 代谢机制,让 Agent 真正"记得用户、积累经验、自动遗忘过时信息"。

常见问题与避坑

Q1:模型上下文窗口都有 1M Token 了,还需要长期记忆吗?
需要。窗口再大也解决不了两个问题:一是跨会话(关掉就没了),二是成本与 Lost in the Middle(塞满 1M 既贵又容易"中间丢失")。长期记忆的本质不是"装更多",而是"在合适的时机只召回最相关的那一小撮"。

Q2:向量记忆召回不准,总召回一些"语义相近但业务无关"的内容,怎么办?
这是典型的"虚假关联"。三个对策:①召回后加一层 rerank(重排) 模型;②检索时叠加 业务过滤条件(如按 category、时间范围、用户 id 过滤);③对召回结果做一次 LLM 相关性判定,过滤掉无关项再注入。

Q3:代谢机制(反思/合并/遗忘)多久跑一次?
建议异步、离线跑,别卡在用户对话的关键路径上。反思可以在每次会话结束后触发;合并和遗忘属于"大扫除",按数据量每天或每周跑一次批处理即可。

Q4:记忆是不是越多越好?
不是。过时、冲突、重复的记忆会严重干扰 Agent 判断。宁可保持一个"小而精"的记忆库,也不要一个"大而乱"的流水账。遗忘和合并的重要性,不亚于写入。

Q5:AGENTS.md 会不会越长越拖慢 Agent?
会。这也是它要坚持"错误驱动进化"的原因——只沉淀被验证过的防错规则,而不是堆砌一份理想化的长文档。过长的 System Prompt 本身就会把 Agent 推向"上下文焦虑"。定期回顾:哪些规则是因为"模型以前做不好"才加的,现在是否可以精简。


总结

回到开篇的三个痛点:跨会话失忆、长对话变笨、记忆越攒越乱——它们的解法都藏在"分层"二字里。

短期记忆(工作台)          长期记忆(笔记本)
├─ 滑动窗口                  ├─ 写入:会话后语义提纯
├─ 摘要压缩                  ├─ 读取:会话前向量检索注入
└─ 重型结果卸载              ├─ 向量记忆(海量私有经验)
                             ├─ Markdown 记忆(硬性规矩,错误驱动进化)
                             └─ 代谢机制(反思 → 合并 → 遗忘)

进阶路径:L1 管上下文 → L2 沉淀规矩 → L3 经验闭环

记住记忆系统的终极目标,是让 Agent 清楚三件事:

  1. 它知道什么(事实)——靠长期记忆沉淀;
  2. 它学到了什么(经验)——靠自我反思提取元知识;
  3. 此刻正在处理什么(状态)——靠短期记忆守住当前任务上下文。

下次再遇到 Agent"健忘"或"逻辑断层",先别急着换模型——先问问自己:它的"记忆系统",分层了吗?代谢了吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值