Agent 记忆系统:短期记忆、长期记忆与向量库

  created  by  鱼鱼 {{tag}}
创建于 2026年09月30日 14:28:48 最后修改于 2026年09月30日 15:15:24

大模型本身是“无状态”的:每次请求都是一次全新的开始。Agent 之所以看起来“记得”你,是因为工程上做了 记忆系统。这篇文章梳理记忆的分类、常见实现方式、存储选型,并用可运行的代码演示短期记忆的摘要压缩与长期记忆的写入、召回,最后总结踩坑点和检查清单。

一、背景:为什么 Agent 需要记忆

先看三个很常见的场景:

  • 场景 A:用户上周说过“我的项目用 Java 17,不要给我 Java 8 的写法”。这周开新会话,Agent 又给出了 Java 8 的代码。用户会觉得“它根本不记得我”。

  • 场景 B:一个长任务跑了 40 步,到后半段 Agent 忘了最开始的约束“不要修改生产配置”。这是同一会话内的遗忘,因为早期内容被裁剪或淹没。

  • 场景 C:Agent 上次成功解决过某类部署故障,这次遇到类似问题,又从头试错一遍,白白浪费步数和费用。

三个场景对应三种不同的记忆需求:跨会话的偏好、会话内的关键约束、过往经验的复用。如果用一个“把所有东西都塞进提示词”的办法去解决,很快就会遇到上下文长度、成本和噪声问题。所以记忆系统的核心不是“存得多”,而是 存什么、存在哪、什么时候取出来。

二、记忆的三种层次

可以借用人类记忆的类比来理解(这只是类比,不是严格的认知科学定义):

  • 短期记忆(工作记忆):当前对话的上下文,直接放在提示词里。容量受上下文窗口限制。

  • 长期记忆:跨会话保存的信息,比如用户偏好、项目背景,需要外部存储。

  • 情景/经验记忆:记录“过去做过什么、成功或失败的经验”,用于以后复用。

  用户输入
     │
     ▼
 ┌────────────────────────────────────┐
 │  上下文窗口(短期记忆)             │
 │  系统提示 + 最近 N 轮 + 检索结果    │
 └────────────────────────────────────┘
     ▲                    │
     │ 检索(Recall)        │ 写入(Memorize)
     │                    ▼
 ┌────────────────────────────────────┐
 │  外部存储(长期记忆)               │
 │  向量库 / 关系库 / 键值库 / 文件     │
 └────────────────────────────────────┘

一个直观的对应关系如下:

记忆类型 存放位置 生命周期 典型内容 主要风险
短期记忆 上下文窗口(提示词) 一次会话或一次任务 最近对话、当前计划、工具结果 超长、被淹没、成本高
长期记忆 外部存储 跨会话,直到被删除 用户偏好、项目背景、知识片段 过期、噪声、隐私
经验记忆 外部存储 跨任务 成功/失败的处理步骤与教训 经验不适用于新场景

三、短期记忆:别让上下文无限膨胀

最简单的做法是把全部历史都发给模型,但很快会遇到长度和成本问题。常见策略:

  1. 滑动窗口:只保留最近 N 轮,实现最简单,但会丢失早期关键信息。

  2. 摘要压缩:对旧对话让模型生成摘要,再把摘要 + 最近几轮一起发送。

  3. 关键信息置顶:把用户目标、约束条件固定放在系统提示里,不随窗口滑走。

  4. 工具结果裁剪:把冗长的工具输出只留结论,原文存到外部按需读取。

策略对比

策略 实现难度 信息保真度 额外成本 适用场景
滑动窗口 低 低(早期信息直接丢) 无 闲聊、短任务
摘要压缩 中 中(取决于摘要质量) 多一次模型调用 长对话、长任务
关键信息置顶 低 高(仅限少量关键信息) 占用固定长度 有明确约束的任务
工具结果裁剪 中 取决于裁剪规则 需存储原文 工具输出很长的 Agent

实际项目中往往是组合使用:系统提示中置顶关键约束,中间是旧历史的摘要,最后是最近几轮原文。

代码:带摘要的短期记忆(Python)

from dataclasses import dataclass, field

@dataclass
class ShortTermMemory:
    pinned: list[str] = field(default_factory=list)   # 置顶信息:目标、约束
    summary: str = ""                                  # 旧对话的摘要
    recent: list[dict] = field(default_factory=list)   # 最近若干条原始消息
    max_recent: int = 8                                # 最近消息保留条数

    def add(self, role: str, content: str):
        self.recent.append({"role": role, "content": content})
        if len(self.recent) > self.max_recent:
            self._compress()

    def _compress(self):
        # 把较早的一半消息交给模型做摘要,其余保留原文
        half = len(self.recent) // 2
        old, self.recent = self.recent[:half], self.recent[half:]
        prompt = (
            "请把以下对话压缩成简短摘要,保留:用户目标、已做出的决定、未解决的问题。\n"
            f"已有摘要:{self.summary}\n新增对话:{old}"
        )
        self.summary = llm(prompt)          # llm() 为你自己封装的模型调用

    def build_messages(self) -> list[dict]:
        system = "你是助手。\n" + "\n".join(f"- {p}" for p in self.pinned)
        msgs = [{"role": "system", "content": system}]
        if self.summary:
            msgs.append({"role": "system", "content": f"此前对话摘要:{self.summary}"})
        return msgs + self.recent

需要注意:摘要本身会丢信息,而且摘要由模型生成,可能出现遗漏甚至曲解。对必须逐字保留的内容(例如用户给出的精确数值、文件路径),应放进“置顶”区域或另存,而不是依赖摘要。

四、长期记忆:向量检索的基本套路

向量库负责“按语义找相似内容”。流程分为写入与读取两步:

  • 写入:把文本切块 → 用嵌入模型(Embedding)转成向量 → 连同原文和 元数据存入向量库。

  • 读取:把当前问题转成向量 → 找最相近的 k 条 → 拼进提示词。

def memorize(user_id: str, text: str):
    vec = embed(text)                       # 嵌入模型,返回浮点向量
    store.add(id=new_id(), vector=vec, payload={
        "user_id": user_id, "text": text, "ts": now()
    })

def recall(user_id: str, query: str, k: int = 4):
    hits = store.search(vector=embed(query), top_k=k,
                        filter={"user_id": user_id})   # 务必按用户隔离
    return [h.payload["text"] for h in hits]

一个不依赖外部服务的最小示例

为了让你看清原理,下面用纯 Python 实现一个“玩具版”记忆库:用余弦相似度做检索,并加入“是否值得记”的判断和去重。嵌入函数 embed 需要你自己接入真实的嵌入模型。

import math, time, uuid

def cosine(a: list[float], b: list[float]) -> float:
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a))
    nb = math.sqrt(sum(y * y for y in b))
    return dot / (na * nb + 1e-9)

class ToyMemoryStore:
    def __init__(self):
        self.items = []     # 生产环境请换成真正的向量库

    def add(self, user_id: str, text: str, kind: str = "preference"):
        vec = embed(text)
        # 去重:与已有记忆过于相似就更新,而不是重复写入
        for it in self.items:
            if it["user_id"] == user_id and cosine(it["vec"], vec) > 0.95:
                it.update(text=text, vec=vec, ts=time.time())
                return it["id"]
        item = {"id": str(uuid.uuid4()), "user_id": user_id, "text": text,
                "kind": kind, "vec": vec, "ts": time.time()}
        self.items.append(item)
        return item["id"]

    def search(self, user_id: str, query: str, k: int = 4, min_score: float = 0.6):
        qv = embed(query)
        scored = [(cosine(it["vec"], qv), it) for it in self.items
                  if it["user_id"] == user_id]
        scored.sort(key=lambda x: x[0], reverse=True)
        # 设置相似度下限,避免把不相关的内容硬塞进提示词
        return [it["text"] for s, it in scored[:k] if s >= min_score]

    def delete_user(self, user_id: str):        # 支持“被遗忘权”
        self.items = [it for it in self.items if it["user_id"] != user_id]

注意 0.95 和 0.6 这两个阈值只是示意,不同嵌入模型的相似度分布差别很大,必须用你自己的数据校准。

什么值得写入长期记忆

让 Agent 每句话都写入记忆,很快就会被噪声淹没。可以用“写入门槛”来过滤:

  1. 用户明确要求记住(“以后都用中文回答”);

  2. 稳定的偏好、身份、项目背景(技术栈、团队规范);

  3. 任务中得出的、以后会复用的结论(某个故障的根因与修复方法);

  4. 不要写入:一次性的闲聊、敏感信息(密码、证件号)、尚未验证的模型推测。

五、Java 视角:用接口抽象记忆,方便替换后端

在 Java 项目里,建议先定义一个记忆接口,业务代码只依赖接口,底层可以从内存实现逐步换成数据库或向量库:

import java.time.Instant;
import java.util.List;

/** 一条长期记忆 */
public record MemoryItem(String id, String userId, String text,
                         String kind, Instant createdAt) {}

/** 长期记忆的抽象:业务只依赖它,不关心底层是向量库还是数据库 */
public interface LongTermMemory {
    /** 写入一条记忆,返回记忆 id */
    String save(String userId, String text, String kind);

    /** 按语义召回最相关的 k 条,必须带 userId 做隔离 */
    List<MemoryItem> recall(String userId, String query, int k);

    /** 删除某用户的全部记忆(合规要求) */
    void deleteAll(String userId);
}

/** 组装提示词时使用:把召回结果拼成一个明确标注的片段 */
public class MemoryPromptBuilder {
    private final LongTermMemory memory;

    public MemoryPromptBuilder(LongTermMemory memory) {
        this.memory = memory;
    }

    public String buildMemoryBlock(String userId, String userInput) {
        List<MemoryItem> items = memory.recall(userId, userInput, 4);
        if (items.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder("【与用户相关的已知信息(可能过时,仅供参考)】\n");
        for (MemoryItem it : items) {
            sb.append("- ").append(it.text()).append('\n');
        }
        return sb.toString();
    }
}

这里特意在提示词里写上“可能过时,仅供参考”:让模型知道记忆不是绝对事实,当用户当前的话与记忆冲突时,以当前输入为准。

六、不同存储的适用场景

存储类型 适合保存 特点
向量库 非结构化文本、知识片段 语义相似检索,结果是“近似”的,不保证精确
关系数据库 用户档案、订单等结构化事实 精确查询,易于更新与删除
键值/缓存 会话状态、临时中间结果 读写快,适合短期数据
文件/文档 长文本笔记、任务记录 简单直观,便于人工查看和修改

提示:向量检索不是万能的。对于“用户的邮箱是什么”这类精确事实,关系库或键值库比向量库可靠得多。

一种务实的做法是 混合存储:精确事实放关系库/键值库,非结构化的经验和知识放向量库;检索时两边并行查,再合并进提示词。

七、完整走一遍:一次带记忆的对话

以“个人开发助手”为例,看记忆在一轮对话里如何流动:

  1. 用户:“帮我写个定时任务,记得我们项目用 Java 17 和 Spring Boot 3。”

  2. Agent 在回答之前,先 recall(user_id, "定时任务"),假设召回一条旧记忆“团队约定:日志使用 SLF4J,不要用 System.out”。

  3. 上下文组装:系统提示 + 置顶约束 + 召回的记忆 + 最近对话 + 当前输入。

  4. 模型生成代码,并遵守了“SLF4J”的约定。

  5. 回答结束后,Agent 判断本轮出现了新的稳定信息(Java 17、Spring Boot 3),调用 memorize 写入;若库中已有类似记忆则更新而不是重复添加。

  6. 一周后用户再次提问,新会话里这些信息会被召回,不需要用户重复交代。

这条链路里最容易出问题的是第 5 步:写入时机和写入内容的判断。建议先用规则(用户明确说“记住”、出现稳定偏好关键词)加人工可审的记忆列表,效果稳定后再引入“让模型判断是否值得记”。

八、遗忘与更新:让记忆“可控地变旧”

只写不删的记忆库,最终会变成一个充满过期信息的垃圾场。遗忘不是缺陷,而是必需的功能。常见的做法有:

  • 按时间衰减:检索时把“越新越优先”作为排序因素之一,而不是只看相似度;

  • 按使用频率保留:长期没有被召回过的低价值记忆,定期归档或删除;

  • 冲突覆盖:发现新信息与旧记忆矛盾(例如用户从 Maven 改用 Gradle),更新旧条目并保留变更时间,而不是并存两条互相矛盾的记忆;

  • 用户可见可改:提供“查看我的记忆 / 删除某条记忆”的入口,既提升信任,也是合规的基本要求。

一个简单的检索打分思路是:最终得分 = 相似度 × 时间权重,其中时间权重随天数递减。具体的衰减曲线没有标准答案,应结合你的业务(偏好类记忆衰减慢,临时任务类记忆衰减快)和评测结果来定。

九、常见坑与解决办法

坑 表现 解决办法
什么都存 记忆越多噪声越大,召回的内容不相关 设定写入门槛;设置相似度下限;定期清理
记忆过期 用户换了技术栈,Agent 仍按旧偏好回答 保留时间戳;冲突时以最新输入为准并更新记忆
跨用户泄露 A 用户的记忆出现在 B 用户的回答里 写入和检索都带 user_id,并在存储层强制过滤
检索不准 问题与记忆语义相关但没被召回 调整切块大小、尝试改写查询、混合关键词检索
摘要丢关键信息 长任务后半段忘记早期约束 关键约束置顶,不依赖摘要
记忆“污染” 模型把错误推测写进记忆并反复使用 只写入经用户确认或工具验证的信息;记录来源
无法删除 用户要求删除但找不到对应记忆 为记忆保存来源与用户标识,提供按用户删除的接口

十、记忆系统检查清单

  • ☐ 明确区分了短期记忆、长期记忆,各自的容量与生命周期有定义。

  • ☐ 关键约束放在置顶区域,而不是只存在于会被摘要的历史中。

  • ☐ 长期记忆有写入门槛,敏感信息不入库。

  • ☐ 所有读写都按用户隔离,并且隔离发生在存储层而不仅是应用层。

  • ☐ 每条记忆带时间戳、来源,支持更新与删除。

  • ☐ 召回结果设置了条数上限和相似度下限。

  • ☐ 提示词中标明“记忆可能过时”,冲突时以当前输入为准。

  • ☐ 用一批真实问题评估过召回效果,而不是只看主观感受。

十一、常见问题(FAQ)

Q1:上下文窗口越来越大,还需要记忆系统吗?仍然需要。窗口变大只是放宽了限制,但成本和延迟依然随长度增加,而且过长的上下文里关键信息更容易被淹没。此外,跨会话的持久化靠窗口解决不了,必须有外部存储。

Q2:一定要上向量库吗?不一定。如果记忆条数很少(几十条),直接全部放进提示词,或者用关键词检索,往往就足够了。当记忆量大到无法全部放入、且需要语义匹配时,再引入向量库。

Q3:切块应该多大?没有通用答案。块太大,无关内容会稀释相关信息;块太小,又会丢失上下文。建议从“按段落或按语义单元切分,并保留少量重叠”开始,用真实问题做小规模评测后再调整。

Q4:记忆应该由模型自己管理,还是由程序规则管理?两种方式各有取舍:模型管理灵活但不稳定,规则管理稳定但僵硬。推荐起步阶段以规则为主、模型辅助,并保留人工可查看、可编辑记忆的入口。

十二、小结与下一步

好的记忆系统 = 合理的分层 + 有选择地写入 + 精准地召回 + 可控的遗忘。先从“滑动窗口 + 摘要 + 置顶约束”做起,再按需引入向量库与混合存储。

下一步可以这样走:

  1. 梳理你的 Agent 当前有哪些信息被反复询问,列出“值得记”的清单;

  2. 实现一个带用户隔离的长期记忆接口,先用简单存储跑通;

  3. 准备 20 条“应该被召回”的测试问题,评估召回效果;

  4. 再去看 规划与任务分解——规划的中间状态,本身也是一种需要被好好保存的“记忆”。

评论区
评论
{{comment.creator}}
{{comment.createTime}} {{comment.index}}楼
评论

Agent 记忆系统:短期记忆、长期记忆与向量库

Agent 记忆系统:短期记忆、长期记忆与向量库

大模型本身是“无状态”的:每次请求都是一次全新的开始。Agent 之所以看起来“记得”你,是因为工程上做了 记忆系统。这篇文章梳理记忆的分类、常见实现方式、存储选型,并用可运行的代码演示短期记忆的摘要压缩与长期记忆的写入、召回,最后总结踩坑点和检查清单。

一、背景:为什么 Agent 需要记忆

先看三个很常见的场景:

三个场景对应三种不同的记忆需求:跨会话的偏好、会话内的关键约束、过往经验的复用。如果用一个“把所有东西都塞进提示词”的办法去解决,很快就会遇到上下文长度、成本和噪声问题。所以记忆系统的核心不是“存得多”,而是 存什么、存在哪、什么时候取出来。

二、记忆的三种层次

可以借用人类记忆的类比来理解(这只是类比,不是严格的认知科学定义):

  用户输入
     │
     ▼
 ┌────────────────────────────────────┐
 │  上下文窗口(短期记忆)             │
 │  系统提示 + 最近 N 轮 + 检索结果    │
 └────────────────────────────────────┘
     ▲                    │
     │ 检索(Recall)        │ 写入(Memorize)
     │                    ▼
 ┌────────────────────────────────────┐
 │  外部存储(长期记忆)               │
 │  向量库 / 关系库 / 键值库 / 文件     │
 └────────────────────────────────────┘

一个直观的对应关系如下:

记忆类型 存放位置 生命周期 典型内容 主要风险
短期记忆 上下文窗口(提示词) 一次会话或一次任务 最近对话、当前计划、工具结果 超长、被淹没、成本高
长期记忆 外部存储 跨会话,直到被删除 用户偏好、项目背景、知识片段 过期、噪声、隐私
经验记忆 外部存储 跨任务 成功/失败的处理步骤与教训 经验不适用于新场景

三、短期记忆:别让上下文无限膨胀

最简单的做法是把全部历史都发给模型,但很快会遇到长度和成本问题。常见策略:

  1. 滑动窗口:只保留最近 N 轮,实现最简单,但会丢失早期关键信息。

  2. 摘要压缩:对旧对话让模型生成摘要,再把摘要 + 最近几轮一起发送。

  3. 关键信息置顶:把用户目标、约束条件固定放在系统提示里,不随窗口滑走。

  4. 工具结果裁剪:把冗长的工具输出只留结论,原文存到外部按需读取。

策略对比

策略 实现难度 信息保真度 额外成本 适用场景
滑动窗口 低 低(早期信息直接丢) 无 闲聊、短任务
摘要压缩 中 中(取决于摘要质量) 多一次模型调用 长对话、长任务
关键信息置顶 低 高(仅限少量关键信息) 占用固定长度 有明确约束的任务
工具结果裁剪 中 取决于裁剪规则 需存储原文 工具输出很长的 Agent

实际项目中往往是组合使用:系统提示中置顶关键约束,中间是旧历史的摘要,最后是最近几轮原文。

代码:带摘要的短期记忆(Python)

from dataclasses import dataclass, field

@dataclass
class ShortTermMemory:
    pinned: list[str] = field(default_factory=list)   # 置顶信息:目标、约束
    summary: str = ""                                  # 旧对话的摘要
    recent: list[dict] = field(default_factory=list)   # 最近若干条原始消息
    max_recent: int = 8                                # 最近消息保留条数

    def add(self, role: str, content: str):
        self.recent.append({"role": role, "content": content})
        if len(self.recent) > self.max_recent:
            self._compress()

    def _compress(self):
        # 把较早的一半消息交给模型做摘要,其余保留原文
        half = len(self.recent) // 2
        old, self.recent = self.recent[:half], self.recent[half:]
        prompt = (
            "请把以下对话压缩成简短摘要,保留:用户目标、已做出的决定、未解决的问题。\n"
            f"已有摘要:{self.summary}\n新增对话:{old}"
        )
        self.summary = llm(prompt)          # llm() 为你自己封装的模型调用

    def build_messages(self) -> list[dict]:
        system = "你是助手。\n" + "\n".join(f"- {p}" for p in self.pinned)
        msgs = [{"role": "system", "content": system}]
        if self.summary:
            msgs.append({"role": "system", "content": f"此前对话摘要:{self.summary}"})
        return msgs + self.recent

需要注意:摘要本身会丢信息,而且摘要由模型生成,可能出现遗漏甚至曲解。对必须逐字保留的内容(例如用户给出的精确数值、文件路径),应放进“置顶”区域或另存,而不是依赖摘要。

四、长期记忆:向量检索的基本套路

向量库负责“按语义找相似内容”。流程分为写入与读取两步:

def memorize(user_id: str, text: str):
    vec = embed(text)                       # 嵌入模型,返回浮点向量
    store.add(id=new_id(), vector=vec, payload={
        "user_id": user_id, "text": text, "ts": now()
    })

def recall(user_id: str, query: str, k: int = 4):
    hits = store.search(vector=embed(query), top_k=k,
                        filter={"user_id": user_id})   # 务必按用户隔离
    return [h.payload["text"] for h in hits]

一个不依赖外部服务的最小示例

为了让你看清原理,下面用纯 Python 实现一个“玩具版”记忆库:用余弦相似度做检索,并加入“是否值得记”的判断和去重。嵌入函数 embed 需要你自己接入真实的嵌入模型。

import math, time, uuid

def cosine(a: list[float], b: list[float]) -> float:
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a))
    nb = math.sqrt(sum(y * y for y in b))
    return dot / (na * nb + 1e-9)

class ToyMemoryStore:
    def __init__(self):
        self.items = []     # 生产环境请换成真正的向量库

    def add(self, user_id: str, text: str, kind: str = "preference"):
        vec = embed(text)
        # 去重:与已有记忆过于相似就更新,而不是重复写入
        for it in self.items:
            if it["user_id"] == user_id and cosine(it["vec"], vec) > 0.95:
                it.update(text=text, vec=vec, ts=time.time())
                return it["id"]
        item = {"id": str(uuid.uuid4()), "user_id": user_id, "text": text,
                "kind": kind, "vec": vec, "ts": time.time()}
        self.items.append(item)
        return item["id"]

    def search(self, user_id: str, query: str, k: int = 4, min_score: float = 0.6):
        qv = embed(query)
        scored = [(cosine(it["vec"], qv), it) for it in self.items
                  if it["user_id"] == user_id]
        scored.sort(key=lambda x: x[0], reverse=True)
        # 设置相似度下限,避免把不相关的内容硬塞进提示词
        return [it["text"] for s, it in scored[:k] if s >= min_score]

    def delete_user(self, user_id: str):        # 支持“被遗忘权”
        self.items = [it for it in self.items if it["user_id"] != user_id]

注意 0.95 和 0.6 这两个阈值只是示意,不同嵌入模型的相似度分布差别很大,必须用你自己的数据校准。

什么值得写入长期记忆

让 Agent 每句话都写入记忆,很快就会被噪声淹没。可以用“写入门槛”来过滤:

  1. 用户明确要求记住(“以后都用中文回答”);

  2. 稳定的偏好、身份、项目背景(技术栈、团队规范);

  3. 任务中得出的、以后会复用的结论(某个故障的根因与修复方法);

  4. 不要写入:一次性的闲聊、敏感信息(密码、证件号)、尚未验证的模型推测。

五、Java 视角:用接口抽象记忆,方便替换后端

在 Java 项目里,建议先定义一个记忆接口,业务代码只依赖接口,底层可以从内存实现逐步换成数据库或向量库:

import java.time.Instant;
import java.util.List;

/** 一条长期记忆 */
public record MemoryItem(String id, String userId, String text,
                         String kind, Instant createdAt) {}

/** 长期记忆的抽象:业务只依赖它,不关心底层是向量库还是数据库 */
public interface LongTermMemory {
    /** 写入一条记忆,返回记忆 id */
    String save(String userId, String text, String kind);

    /** 按语义召回最相关的 k 条,必须带 userId 做隔离 */
    List<MemoryItem> recall(String userId, String query, int k);

    /** 删除某用户的全部记忆(合规要求) */
    void deleteAll(String userId);
}

/** 组装提示词时使用:把召回结果拼成一个明确标注的片段 */
public class MemoryPromptBuilder {
    private final LongTermMemory memory;

    public MemoryPromptBuilder(LongTermMemory memory) {
        this.memory = memory;
    }

    public String buildMemoryBlock(String userId, String userInput) {
        List<MemoryItem> items = memory.recall(userId, userInput, 4);
        if (items.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder("【与用户相关的已知信息(可能过时,仅供参考)】\n");
        for (MemoryItem it : items) {
            sb.append("- ").append(it.text()).append('\n');
        }
        return sb.toString();
    }
}

这里特意在提示词里写上“可能过时,仅供参考”:让模型知道记忆不是绝对事实,当用户当前的话与记忆冲突时,以当前输入为准。

六、不同存储的适用场景

存储类型 适合保存 特点
向量库 非结构化文本、知识片段 语义相似检索,结果是“近似”的,不保证精确
关系数据库 用户档案、订单等结构化事实 精确查询,易于更新与删除
键值/缓存 会话状态、临时中间结果 读写快,适合短期数据
文件/文档 长文本笔记、任务记录 简单直观,便于人工查看和修改

提示:向量检索不是万能的。对于“用户的邮箱是什么”这类精确事实,关系库或键值库比向量库可靠得多。

一种务实的做法是 混合存储:精确事实放关系库/键值库,非结构化的经验和知识放向量库;检索时两边并行查,再合并进提示词。

七、完整走一遍:一次带记忆的对话

以“个人开发助手”为例,看记忆在一轮对话里如何流动:

  1. 用户:“帮我写个定时任务,记得我们项目用 Java 17 和 Spring Boot 3。”

  2. Agent 在回答之前,先 recall(user_id, "定时任务"),假设召回一条旧记忆“团队约定:日志使用 SLF4J,不要用 System.out”。

  3. 上下文组装:系统提示 + 置顶约束 + 召回的记忆 + 最近对话 + 当前输入。

  4. 模型生成代码,并遵守了“SLF4J”的约定。

  5. 回答结束后,Agent 判断本轮出现了新的稳定信息(Java 17、Spring Boot 3),调用 memorize 写入;若库中已有类似记忆则更新而不是重复添加。

  6. 一周后用户再次提问,新会话里这些信息会被召回,不需要用户重复交代。

这条链路里最容易出问题的是第 5 步:写入时机和写入内容的判断。建议先用规则(用户明确说“记住”、出现稳定偏好关键词)加人工可审的记忆列表,效果稳定后再引入“让模型判断是否值得记”。

八、遗忘与更新:让记忆“可控地变旧”

只写不删的记忆库,最终会变成一个充满过期信息的垃圾场。遗忘不是缺陷,而是必需的功能。常见的做法有:

一个简单的检索打分思路是:最终得分 = 相似度 × 时间权重,其中时间权重随天数递减。具体的衰减曲线没有标准答案,应结合你的业务(偏好类记忆衰减慢,临时任务类记忆衰减快)和评测结果来定。

九、常见坑与解决办法

坑 表现 解决办法
什么都存 记忆越多噪声越大,召回的内容不相关 设定写入门槛;设置相似度下限;定期清理
记忆过期 用户换了技术栈,Agent 仍按旧偏好回答 保留时间戳;冲突时以最新输入为准并更新记忆
跨用户泄露 A 用户的记忆出现在 B 用户的回答里 写入和检索都带 user_id,并在存储层强制过滤
检索不准 问题与记忆语义相关但没被召回 调整切块大小、尝试改写查询、混合关键词检索
摘要丢关键信息 长任务后半段忘记早期约束 关键约束置顶,不依赖摘要
记忆“污染” 模型把错误推测写进记忆并反复使用 只写入经用户确认或工具验证的信息;记录来源
无法删除 用户要求删除但找不到对应记忆 为记忆保存来源与用户标识,提供按用户删除的接口

十、记忆系统检查清单

十一、常见问题(FAQ)

Q1:上下文窗口越来越大,还需要记忆系统吗?仍然需要。窗口变大只是放宽了限制,但成本和延迟依然随长度增加,而且过长的上下文里关键信息更容易被淹没。此外,跨会话的持久化靠窗口解决不了,必须有外部存储。

Q2:一定要上向量库吗?不一定。如果记忆条数很少(几十条),直接全部放进提示词,或者用关键词检索,往往就足够了。当记忆量大到无法全部放入、且需要语义匹配时,再引入向量库。

Q3:切块应该多大?没有通用答案。块太大,无关内容会稀释相关信息;块太小,又会丢失上下文。建议从“按段落或按语义单元切分,并保留少量重叠”开始,用真实问题做小规模评测后再调整。

Q4:记忆应该由模型自己管理,还是由程序规则管理?两种方式各有取舍:模型管理灵活但不稳定,规则管理稳定但僵硬。推荐起步阶段以规则为主、模型辅助,并保留人工可查看、可编辑记忆的入口。

十二、小结与下一步

好的记忆系统 = 合理的分层 + 有选择地写入 + 精准地召回 + 可控的遗忘。先从“滑动窗口 + 摘要 + 置顶约束”做起,再按需引入向量库与混合存储。

下一步可以这样走:

  1. 梳理你的 Agent 当前有哪些信息被反复询问,列出“值得记”的清单;

  2. 实现一个带用户隔离的长期记忆接口,先用简单存储跑通;

  3. 准备 20 条“应该被召回”的测试问题,评估召回效果;

  4. 再去看 规划与任务分解——规划的中间状态,本身也是一种需要被好好保存的“记忆”。


Agent 记忆系统:短期记忆、长期记忆与向量库2026-09-30鱼鱼

{{commentTitle}}

评论   ctrl+Enter 发送评论