Agent 记忆层落地实战:抽取、整合、存储、检索,为什么大上下文窗口救不了你

从一次全量上下文翻车说起

去年我们给一家跨境电商做客服 Agent,用户平均交互跨度超过三个月。团队最初方案很朴素:把全部历史对话塞进 prompt。GPT-4-turbo 的 128k 窗口看起来够用。上线两周后问题集中爆发------用户半年前提到的退货原因被当成当前诉求,系统反复追问已经确认过的订单号,而真正重要的"这个用户对某品牌过敏"这条三个月前的信息,被淹没在几十条物流查询记录里。全量上下文不是记忆,它只是没有筛选的档案堆。

LongMemEval 基准的数据印证了这一点:该基准的长对话上下文平均约 115000 tokens,全量上下文基线在 DMR 上大约 94.4%,而会话摘要基线只有约 78.6%。更关键的是,这类研究反复观察到同一个现象:当关键证据被埋在十万级 token 的历史里,模型的准确率会明显下滑,长上下文并不等于长记忆。窗口装得下,注意力装不住,这是两个问题。

三类记忆与四段流水线

记忆层的第一步不是写检索代码,而是想清楚要存什么。Tulving 在 1972 年正式区分了情景记忆与语义记忆:情景记忆指向个人经历的事件及其时空关系,语义记忆则是关于词语、概念和规则的组织化知识。CoALA 框架在这个二分法上补上了程序记忆------隐式的 LLM 权重知识和显式的代码、技能、工作流。落到工程上,这三类记忆的存储位置、更新频率和检索方式完全不同。

记忆类型 认知科学来源 工程载体 典型更新频率 检索触发条件
语义记忆 Tulving 1972 结构化事实表、知识图谱节点 低频,事实级写入 实体或属性匹配
情景记忆 Tulving 1972 事件日志、会话摘要、向量库 高频,每轮对话追加 时间窗口加语义相似
程序记忆 CoALA 2024 版本化技能文件、prompt 模板、工具定义 极低频,人工或 CI 审批 意图识别后显式调用

四段式流水线把这三类记忆串起来。抽取阶段从原始会话中识别值得长期保存的信号,整合阶段去重、消解冲突、建立关联,存储阶段按记忆类型路由到不同后端,检索阶段根据当前任务组合向量相似度和图遍历。这个顺序不能颠倒------先有写入质量,才有检索质量。

阶段 核心动作 失败模式 可观测指标
抽取 实体识别、事实抽取、意图分类 噪声写入、漏抽关键事实 抽取精确率、写入量日环比
整合 去重、冲突消解、时效标记 旧事实未失效、矛盾并存 冲突检出率、合并延迟
存储 按类型路由、建立索引、版本化 存储膨胀、索引漂移 单用户记忆条数、存储成本
检索 混合召回、重排序、预算控制 召回不足、token 超预算 命中率、检索延迟 p95

向量与图的取舍不是二选一

向量库适合语义记忆的模糊匹配。用户说"上次那个蓝色的包",向量检索能召回"蓝色手提包"的实体记录。但向量库的致命缺陷是关系盲区------它不知道"用户 A 的过敏品牌"和"用户 A 三个月前投诉的批次号"之间存在因果链。一份 2026 年的上下文工程行业调查显示,54% 的受访者表示孤立的向量存储限制了 Agent 的实际效果。同一份调查还显示 98% 的团队认为上下文是 Agent 的核心瓶颈,但只有 2% 报告 Agent 能可靠跨系统导航。

图数据库补的是关系那部分。Zep 的 Graphiti 用时间感知知识图谱同时维护事件时间和摄入时间两条时间线,新事实覆盖旧事实时不删除历史,而是标记失效区间。这让"用户在上个季度用的是哪个支付方式"这类时间切片查询成为可能。

维度 向量检索 图检索 混合方案
匹配方式 语义相似度 关系路径遍历 向量召回后图扩展
时间语义 弱,依赖元数据过滤 原生双时间轴,边级有效期 向量给候选,图给时序校验
写入成本 低,embedding 加索引 高,需实体解析和边维护 中等,向量快写加图异步补全
适合记忆类型 语义记忆、情景摘要 语义记忆中的关系事实 语义加情景联合查询
代表实现 通用向量库 Zep Graphiti、HippoRAG Mem0g

HippoRAG 的层次化检索走了另一条路:把开放知识图谱和 Personalized PageRank 结合起来,用图遍历做多跳召回,弥补向量检索在"需要串联多个事实"场景下的不足。MemGPT 则从操作系统借来分页隐喻,让 Agent 在有限主上下文和外部存储之间主动换入换出信息,而不是被动等待全量注入。这两种思路的共同点是:把检索从一次性的向量查询升级为有预算意识的多步过程。

写一条记忆到底要经过什么

抽取不是把整段对话存下来。NVIDIA 的 NemoClaw 记忆驱动 Agent 把证据和判断做了物理分离:知识层用 Markdown 自模型存人员、项目、优先级和工作模式,SQLite 账本存义务、排名、更正和审计事件。这个设计的关键在于,Markdown 页面写的是"这个协作者偏好 Slack"这样的衍生判断,而原始消息作为证据保留在会话记录里,永不覆盖。

下面的 Python 示例展示一个最小抽取写入与混合检索流程。依赖仅为标准库和一个假设的 embedding 函数,重点看控制流而不是具体实现。

ini 复制代码
import hashlib
import time

class MemoryEntry:
    def __init__(self, kind, key, value, source_msg_id, confidence=1.0):
        self.kind = kind
        self.key = key
        self.value = value
        self.source_msg_id = source_msg_id
        self.confidence = confidence
        self.created_at = time.time()
        self.superseded_by = None

    def fingerprint(self):
        raw = f"{self.kind}:{self.key}"
        return hashlib.sha256(raw.encode()).hexdigest()[:16]

def extract_and_integrate(raw_message, existing_store):
    candidates = extract_facts(raw_message)
    written = []
    for cand in candidates:
        fp = cand.fingerprint()
        old = existing_store.get(fp)
        if old and old.value == cand.value:
            old.confidence = min(1.0, old.confidence + 0.1)
            written.append(old)
            continue
        if old and old.value != cand.value:
            old.superseded_by = cand.fingerprint()
            cand.confidence = 0.7
        existing_store[fp] = cand
        written.append(cand)
    return written

def hybrid_retrieve(query, store, budget=4096):
    vector_hits = embedding_search(query, list(store.values()))
    graph_hits = graph_expand(query, store) if needs_relations(query) else []
    merged = rerank(vector_hits + graph_hits, query)
    selected, used = [], 0
    for m in merged:
        est = estimate_tokens(m.value)
        if used + est > budget:
            break
        selected.append(m)
        used += est
    return selected

def extract_facts(raw_message):
    return []

这个示例里有两个值得注意的控制点。第一,冲突消解发生在写入路径上,不是检索时的事后过滤。旧条目的 superseded_by 字段保留历史链路,新条目携带较低的初始置信度。第二,检索有 token 预算上限,超预算的候选被截断而不是全部塞入。Mem0 的工程数据显示,结构化记忆相对全量上下文有大约 90% 的 token 节省和 91% 的延迟下降,单用户可管理上万条记忆,更新延迟低于 100 毫秒。这个量级的收益不是靠更好的 embedding 模型拿到的,是靠写入路径上的筛选和检索路径上的预算纪律换来的。

账本不只是审计,是判断的锚点

NemoClaw 把 SQLite 账本单独拎出来,不是为了合规摆样子。当 Agent 说"这个任务应该优先处理",这个判断的依据可能来自三处:用户上周口头说了一句优先级、系统根据截止日期自动排名、或者 Agent 从历史模式中推断。如果这三条依据混在同一个向量库里,出错时你无法定位是证据错了、整合规则错了、还是模型在最终决策时偏离了。

SQL 账本的结构应该区分事实记录和决策记录:

sql 复制代码
CREATE TABLE evidence (
    id INTEGER PRIMARY KEY,
    source_type TEXT NOT NULL,
    source_ref TEXT NOT NULL,
    raw_content TEXT NOT NULL,
    ingested_at REAL NOT NULL
);

CREATE TABLE derived_judgment (
    id INTEGER PRIMARY KEY,
    evidence_ids TEXT NOT NULL,
    judgment_type TEXT NOT NULL,
    value TEXT NOT NULL,
    valid_from REAL NOT NULL,
    valid_until REAL,
    confidence REAL DEFAULT 0.5,
    corrected_by INTEGER REFERENCES evidence(id)
);

CREATE INDEX idx_judgment_valid ON derived_judgment(valid_from, valid_until);
CREATE INDEX idx_evidence_source ON evidence(source_type, source_ref);

derived_judgment 表的 evidence_ids 字段指向产生这条判断的证据行,corrected_by 指向用户纠正行为对应的证据。当 Agent 的输出出现偏差时,排查路径是确定的:先查判断行,顺着 evidence_ids 回溯源消息,确认是抽取错误还是模型在检索后的推理中偏离了证据。

NemoClaw 在 Agent Memory Benchmark 上的数据说明了这种分层的价值:整体准确率从 82.8% 提升到 90.9%,变化事实的跟踪从 60.0% 提升到 100.0%。100% 不是模型变聪明了,是时序事实的版本管理把"用户换工作了"和"用户上一份工作是什么"变成了两个独立可查的记录,而不是一个不断被覆盖的文本片段。

去重、冲突与遗忘的工程边界

去重不能只靠 embedding 相似度。两条记忆余弦相似度 0.95,但一条说"用户对花生过敏",另一条说"用户对花生不过敏",语义去重会把它们合并成灾难。工程上需要一条硬规则:negation guard------检测否定词后,相似度再高也不自动合并,而是进入人工审核队列。

冲突消解的策略取决于记忆类型。对于时序事实,Zep 的方案是双时间轴加边级有效期:新事实到来时不给旧事实打删除标记,而是写入 valid_until 时间戳,历史查询仍然可以通过时间点回溯。对于用户偏好,Amazon Bedrock AgentCore Memory 的做法是语义去重加冲突更新:同一偏好的不同表述被合并为一条记录,取值以最新写入为准,但保留变更历史。

遗忘有两个层面。存储层面的遗忘是删除或归档低频访问的记忆条目,防止存储膨胀。检索层面的遗忘是置信度衰减:一条语义记忆如果长时间未被检索命中,其默认置信度逐步下调,在排序中自然靠后。但这套机制必须保留手动豁免通道,否则"用户三个月前说的过敏信息"会因为没被频繁检索而沉底,这正好是记忆系统最该避免的失败模式。Mem0 的后台整合任务把重复记忆合并、过时事实标记为 superseded 而不删除,是当前工程上比较务实的折中。

最小可行落地顺序

不要从图数据库开始。从一张 SQLite 事实表加一个向量索引开始,把写入路径的抽取和冲突消解跑通,用一小批真实会话验证检索命中率。等事实表稳定了两到三周,再引入图结构处理关系查询。程序记忆单独放在 Git 仓库里版本化,不要塞进向量库。

检查清单:每条记忆有指向原始消息的 source_id;冲突消解在写入路径完成而非检索时事后过滤;检索有 token 预算上限;否定检测阻止语义去重误合并;时序事实有 valid_from 和 valid_until;用户纠正触发 evidence 记录而非直接改写判断值;每周有一个后台任务扫描未命中检索的记忆条目的置信度衰减。

相关推荐
steven1581 小时前
第 17 章 组件与配置问题
后端
dkbnull1 小时前
Spring Boot配置文件敏感信息加密解密
spring boot·后端
她的男孩1 小时前
安全同事找我要"上周所有登录失败记录",我查完数据库:一条都没有
java·后端·架构
m0_587383001 小时前
本地同城无人机作业接单系统源码:架构拆解与抢单调度实现
java·小程序·架构·需求分析
高级程序源2 小时前
django校企合作实习基地管理系统82506-计算机课程设计、毕业设计
vue.js·后端·python·mysql·django·课程设计·pygame
盖伦发发2 小时前
Redis 核心教学: 数据结构, 缓存设计, 分布式锁
java·redis·后端·软件工程
卷毛的技术笔记2 小时前
RocketMQ事务消息:我把分布式事务这层窗户纸捅破了
java·分布式·后端·java-rocketmq
.冰块.2 小时前
OpenStack 为什么被叫做云操作系统?核心架构深度解析
架构·open stack
Brilliantwxx2 小时前
【STM32】 从HAL库源码深度解析I2C(源码解析+面试题)
开发语言·stm32·单片机·嵌入式硬件·架构