Agent 记忆系统设计:长期记忆与上下文管理的工程方案

Agent 记忆系统设计:长期记忆与上下文管理的工程方案

一、健忘的 Agent:重复对话背后的记忆缺位

用过 Agent 的人都有个体感:它今天教过的东西,明天就忘。同一个问题反复问,每次都从头解释。长对话进行到第二十轮,前面提到的关键约束已经丢失。这不是模型不够聪明,是它根本没有"记忆"这一层。

大模型本身是无状态的。每一次推理,都是一次从零开始的计算。所谓的"对话历史",其实是把上文重新塞进上下文窗口。窗口一满,老内容就被截断或压缩。

信息流失不可避免。生产场景里,这个问题更刺眼。客服 Agent 记不住用户上周的诉求。编程 Agent 记不住项目约定的代码风格。

运维 Agent 记不住上个月处理过同类故障的方案。每一次都在重复踩坑,每一次都在重新发现轮子。记忆系统要解决的不只是"存下来"。还要解决"什么时候检索"、"检索什么"、"什么时候遗忘"。

存而不检,等于没存;检而不准,等于噪声。不遗忘,记忆库会越来越脏,最终拖垮检索质量。本文探讨 Agent 记忆系统的工程方案。从短期、工作、长期三层记忆切入,落到检索与遗忘策略。目标是让 Agent 真正"积累经验",而不是每次从零开始。

二、分层记忆:短期、工作、长期的协作机制

记忆不是单一容器,而是分层结构。不同层负责不同时间尺度的信息。各司其职,才能兼顾速度与容量。

短期记忆即上下文窗口。存放当前对话的近期消息。读取零延迟,但容量受 token 限制。越靠后的内容越容易被截断。

工作记忆是 scratchpad 草稿区。Agent 在推理过程中产生的中间结论。比如"用户提到的约束"、"已尝试过的方案"。不进对话历史,但参与下一轮决策。

长期记忆是持久化的向量库。跨会话存储事实、偏好、历史经验。通过语义检索按需调取。容量几乎无限,但检索有延迟和成本。

三者协作的链路如下:

flowchart TD A[用户输入] --> B[短期记忆: 上下文窗口] A --> C[工作记忆: scratchpad] A --> D[长期记忆: 向量检索] B --> E[拼装 Prompt] C --> E D --> E E --> F[模型推理] F --> G[输出响应] F --> H[更新工作记忆] F --> I[写入长期记忆] style D fill:#fff3e0 style I fill:#e8f5e9

关键在"按需检索"。不是每次都把长期记忆全塞进 prompt。而是先用用户输入做语义检索,只取相关的几条。否则 prompt 臃肿,token 成本飙升,模型注意力也被稀释。

检索质量决定记忆系统的上限。向量召回不准,相关记忆取不到。召回过多,噪声淹没信号。需要在 top-k 数量、相似度阈值、rerank 之间找平衡。

遗忘同样关键。过时信息不清理,会污染检索结果。比如用户上周说要 A 方案,这周改主意要 B 方案。旧记忆不遗忘,Agent 还会按 A 行事。需要有 TTL、版本标记、显式覆盖等机制。

三、生产级实现:分层记忆管理器

下面用 Python 实现一个分层记忆管理器。聚焦长期记忆的写入与检索,含错误处理与遗忘策略。

python 复制代码
import time
import hashlib
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class MemoryItem:
    """长期记忆的单条记录:内容、元数据、过期时间"""
    content: str
    scope: str                          # 所属会话或用户标识
    tags: list[str] = field(default_factory=list)
    created_at: float = field(default_factory=time.time)
    ttl: Optional[float] = None         # 秒;None 表示永久
    content_hash: str = ""

    def __post_init__(self) -> None:
        # 用内容哈希做去重键,避免相同事实反复写入
        self.content_hash = hashlib.md5(self.content.encode()).hexdigest()


class LongTermMemory:
    """长期记忆库:向量检索 + TTL 遗忘 + 去重"""

    def __init__(self, embed_fn, top_k: int = 5, min_score: float = 0.6) -> None:
        # embed_fn 由调用方注入,便于切换模型与 mock 测试
        self._embed = embed_fn
        self._store: dict[str, MemoryItem] = {}
        self._vectors: dict[str, list[float]] = {}
        self.top_k = top_k
        self.min_score = min_score

    def add(self, item: MemoryItem) -> bool:
        # 去重:同 scope 下相同内容不重复写入
        key = f"{item.scope}:{item.content_hash}"
        if key in self._store:
            return False
        try:
            vec = self._embed(item.content)
        except Exception as e:
            # 嵌入失败不阻断主流程,仅记录并跳过
            # 真实系统接日志与告警,这里简化
            print(f"[memory] embed failed: {e}")
            return False
        self._store[key] = item
        self._vectors[key] = vec
        return True

    def search(self, query: str, scope: str) -> list[MemoryItem]:
        try:
            q_vec = self._embed(query)
        except Exception as e:
            print(f"[memory] query embed failed: {e}")
            return []
        scored = []
        for key, item in list(self._store.items()):
            if not item.scope == scope:
                continue
            # 过期清理:检索时顺手剔除 TTL 到期的记忆
            if item.ttl and time.time() - item.created_at > item.ttl:
                self._store.pop(key, None)
                self._vectors.pop(key, None)
                continue
            score = self._cosine(q_vec, self._vectors[key])
            if score >= self.min_score:
                scored.append((score, item))
        # 按相似度降序取 top_k
        scored.sort(key=lambda x: x[0], reverse=True)
        return [item for _, item in scored[: self.top_k]]

    @staticmethod
    def _cosine(a: list[float], b: list[float]) -> float:
        # 余弦相似度:向量检索的基础度量
        # 真实系统用 faiss 或 milvus,这里内联便于阅读
        dot = sum(x * y for x, y in zip(a, b))
        na = sum(x * x for x in a) ** 0.5
        nb = sum(y * y for y in b) ** 0.5
        if na == 0 or nb == 0:
            return 0.0
        return dot / (na * nb)


if __name__ == "__main__":
    # mock 一个嵌入函数,真实环境替换为线上模型
    def fake_embed(text: str) -> list[float]:
        return [float(len(text)), float(text.count(" "))]

    mem = LongTermMemory(embed_fn=fake_embed, top_k=3, min_score=0.5)
    mem.add(MemoryItem(content="用户偏好 Python", scope="u1", ttl=3600))
    mem.add(MemoryItem(content="用户偏好 Python", scope="u1"))  # 去重,不会写入
    print(f"命中 {len(mem.search('用户喜欢什么语言', 'u1'))} 条")

真实系统会在几个方向上扩展。向量库换成 faiss、milvus 或 pgvector,支撑百万级记忆。检索前加 rerank 模型,提升 top-k 的精度。写入前做事实抽取,只存结构化结论,不存原始对话。遗忘策略叠加"显式覆盖":用户改主意时,主动作废旧记忆。

四、Agent 记忆系统设计的代价与边界

记忆系统能力强,但坑也深。

召回质量不稳。向量相似度高不等于语义相关。"用户喜欢 Python"和"用户讨厌 Python"向量很近。需要配合 rerank 或基于元数据的过滤。

否则错误记忆会误导决策。

记忆污染。Agent 把幻觉或错误结论写进长期记忆。下一轮又检索出来当事实用,错误被放大。写入前必须做事实校验或人工审核。

高风险记忆要标记"待确认"状态。

隐私与合规。长期记忆里可能沉淀用户敏感信息。跨用户共享、跨境传输、留存周期都成问题。需要按 scope 严格隔离,敏感字段脱敏。

并支持按用户请求彻底删除。

成本与延迟。每次检索都要做向量计算和 rerank。高频对话下,记忆系统的开销可能超过推理本身。需要对热查询做缓存,对冷记忆做分层存储。

记忆系统的"治理"比"建库"更难。把记忆写进去只是第一步,真正的工程量在长期运营:定期审计记忆库里的脏数据、监控召回质量指标、处理用户的"遗忘请求"。另一个被忽视的点是"记忆的可解释性":Agent 引用了一条记忆做决策,必须能追溯到来源时间与上下文,否则出错时无法定位是哪条记忆在作祟。最后,记忆系统要预留"关停开关",在召回质量下滑或合规风险出现时,能快速降级为无记忆模式,保住主链路可用。

五、总结

Agent 的记忆系统,本质是让无状态模型获得跨会话的经验积累。机制上用分层结构兼顾速度与容量,用向量检索按需调取。工程上靠去重、TTL、rerank 守住召回质量与成本。落地路线:先上短期记忆管理上下文窗口;再接工作记忆做中间结论暂存;长期记忆用向量库加 rerank;最后补审计、隔离与遗忘治理。记忆不是越多越好,而是越准越好。

相关推荐
phltxy2 小时前
LangGraph智能租房助手实践
大数据·人工智能·python·深度学习·语言模型·langchain
zqrgkjyxgs2 小时前
GEO垂直行业实战:医疗、制造、教育、金融的分行业差异化优化策略
大数据·人工智能·搜索引擎
winrisef2 小时前
ChatGPT/Codex最新版本出错了,无法进入解决方案
人工智能·语言模型·chatgpt·codex
rain_sxr2 小时前
部分 JSON 的增量解析:Function Calling 流式输出的前端执行策略
人工智能
OpenApi.cc2 小时前
Mocode 开发文档平台
人工智能·深度学习·目标检测·自然语言处理·语音识别
Promise微笑2 小时前
电力电缆故障分类与精准定位:物理机制、诊断挑战及前沿技术
人工智能·分类·数据挖掘
GrepowTattu2 小时前
智能护膝与外骨骼设备为什么需要异形定制电池?
人工智能·智能穿戴
夜影风2 小时前
我国AI智能体产业发展洞察:为何能在AI应用层实现“换道超车“
大数据·人工智能
是店小二呀2 小时前
NanoPi R4S怎么搭私人云盘?iStoreOS与WebDAV完整教程
数据库·人工智能