这次我们来看一个和 LLM Agent 长期记忆相关的架构方案:Hierarchical Graph Memory for LLM Agents with Path-level Localization and Rewrite。一句话概括,它解决的是 Agent “记不住、找不准、改不动”的问题:当对话变长、任务变复杂,Agent 需要从大量历史记忆里快速找到完整决策路径,并且能在用户纠正或目标变化后有效更新记忆,而不是把旧信息越攒越乱。
这套机制最核心的特点有三个:第一,记忆不是平铺的向量片段,而是分层的图结构;第二,检索定位的基本单位不是单个节点,而是“路径”,也就是从入口节点到目标节点的一条完整引用链;第三,记忆支持重写,Agent 可以根据新反馈修正已经存下来的路径信息。如果你正在做 RAG 增强、多轮对话系统、Autonomous Agent 的工程落地,或者在做 Agent 记忆方向的论文复现,这篇内容建议直接收藏。
本文会从问题背景入手,拆解层级图记忆的结构设计、路径级定位原理、记忆重写机制,然后给出可参考的实现伪代码、接口设计、实验验证方向和排错清单。文章不涉及具体硬件部署,重点讲清楚这套记忆机制的设计思路和落地路径。
1. 核心能力速览
| 维度 | 说明 |
|---|---|
| 技术类型 | LLM Agent 长期记忆管理机制 |
| 核心机制 | 分层图记忆(Hierarchical Graph Memory) |
| 检索方式 | 路径级定位(Path-level Localization) |
| 更新方式 | 记忆重写(Rewrite) |
| 解决核心问题 | 长对话记忆混乱、多跳关系无法定位、历史记忆无法修正 |
| 适用对象 | LLM Agent、Autonomous Agent、多轮对话系统、Agentic RAG |
| 模型依赖 | 不绑定特定模型,但需要 LLM 具备一定信息抽取和路径生成能力 |
| 硬件需求 | 机制本身主要消耗内存和检索服务资源,具体取决于底座模型部署方式 |
| 实现组件 | 向量检索、图结构存储、LLM 抽取与重写、路径索引 |
| 评估方向 | 检索命中率、路径完整度、多跳任务成功率、重写后稳定性、Token 成本 |
需要说明一点:这个标题是典型的论文式命名,具体实验数据、开源仓库地址、参数配置不能凭空给出。下面讲的所有内容,都是基于标题和机制名称能够直接推导的方法论,以及这类架构在 Agent 工程中的通用实现思路。
2. 为什么 Agent 需要层级图记忆
2.1 长期记忆的三个痛点
先看第一个痛点:上下文窗口有上限。不管模型是 128K 还是 200K 上下文,把整个会话历史全部塞进去都不现实,Token 成本高、噪音大,而且真正关键的早期决策信息会被稀释。
第二个痛点是平面向量检索丢失路径信息。常规做法是把历史记忆切块、做 embedding、存进向量库,需要时召回 Top-K 片段。这种结构只能回答“哪个片段和当前问题相似”,回答不了“这个片段是在哪条决策链上出现的”“它前面是什么、后面是什么”。对单轮问答够用,但对 Agent 这种需要连续执行多个步骤的场景远远不够。
第三个痛点是记忆不能自我更新。已经写入记忆的信息如果被用户纠正,或者被后续证明是错误路径,普通方案只能重新追加一条新记录,旧错误路径还会在后续检索中被召回。结果就是:同一个问题,Agent 每次给出的历史依据可能彼此冲突。
2.2 为什么是“图”而不是“文档”
把记忆组织成图,本质上是把“一段文字”变成“一组节点和边”。节点可以表示实体、事件、决策步骤、用户偏好;边可以表示先后顺序、因果关系、引用关系、相似关系。图结构天然适合表达多跳路径。
例如 Agent 经历过这样一个任务链:
- 用户提出“帮我调研三家云服务商的定价”。
- Agent 调用了官网价格抓取工具。
- 抓取结果不完整,又调用文档解析工具补全。
- 最后生成了对比表格。
- 用户随后纠正“不需要包含带宽费用,只看计算实例”。
这里面每一步都是一个节点,步骤之间的先后关系就是有向边。如果是平铺向量库,第 5 条纠正信息很可能被记成一条独立的文本,和前面 4 步没有任何关联。但在图结构里,这条纠正信息可以反向指向第 2 步和第 3 步的“定价数据采集路径”,下次再遇到类似任务,Agent 能直接避开“采集带宽费用”这个重复动作。
2.3 层级结构解决“规模”问题
如果只有一张巨大的图,检索时照样会迷路。层级图记忆的做法是把图分成多个抽象层次:
- 全局层:存放 Agent 的长期目标、角色设定、稳定偏好。这一层结构稀疏,变化缓慢。
- 场景层:存放某个任务场景或某段会话的核心结论。比如“这次调研的最终结论是选 A 厂商”。这一层是连接全局和事件细节的中间层。
- 事件/路径层:存放每一次具体动作、每一步决策、每一条工具调用链。这一层结构密集,变化频繁。
检索时先从场景层判断“当前属于哪类任务”,再通过场景层定位到相关事件层子图,最后在子图内做路径展开。这样检索范围被逐层缩小,多跳路径的候选集不会爆炸。
从实现角度理解,层级结构类似于给记忆加了“目录索引”。全局层是书的目录,场景层是章节名,事件层是具体段落。平面向量库的问题是只有段落,没有目录;而层级图记忆等于把目录和段落一起存下来,检索时可以按层级逐级索引。
3. 层级图记忆的结构设计
3.1 节点定义
在一个典型的层级图记忆系统中,节点通常包含以下字段:
| 字段 | 说明 |
|---|---|
| node_id | 全局唯一节点 ID |
| level | 层级标识:global / scenario / event |
| content | 节点对应的文本内容或结构化信息 |
| embedding | 节点内容的向量表示,用于语义召回 |
| metadata | 时间戳、来源会话 ID、关联任务 ID、置信度等 |
| parent_id | 父节点 ID,用于层级向上聚合 |
| children_ids | 子节点 ID 列表 |
节点是记忆的最小信息单元。Event 层节点要尽量细粒度,比如一次工具调用的输入输出、一个用户反馈、一个中间结论。不要把一个完整会话压成一个节点,否则路径内部的中间步骤会全部丢失。
3.2 边定义
边定义了节点之间的关联方式,常见的有:
- 时序边(next):A 动作之后执行 B 动作。
- 因果边(causes):B 结果由 A 导致。
- 引用边(references):本路径引用了某个事实节点。
- 版本边(replaces):新路径取代旧路径,用于重写后的历史追溯。
- 归属边(belongs_to):事件节点归属到场景节点。
边本身也可以带权重或置信度,例如用户明确纠正过的路径,权重应当高于未经验证的自动生成路径。检索时可以对权重做归一化,优先展开高置信度边。
3.3 层级之间的动态聚合
层级不是静态不变的。当大量事件节点反复出现在一条路径上,系统可以把它们聚合为一个“场景节点”,并将其中的关键结论提升到场景层。这个操作可以定期执行,也可以由 LLM 在写入记忆时顺便完成。
举个例子,Agent 连续三天都在处理“用户对某 SaaS 产品的工单答疑”,每天产生几十个事件节点。聚合后,场景层会形成一个“该产品常见问题清单”节点,全局层会形成“用户偏好直接给出排障命令”的偏好节点。这样后续同类任务可以直接命中场景层,不需要每次从事件层反推。
4. 路径级定位:从“找片段”到“找路径”
4.1 什么是路径级定位
普通 RAG 检索返回的是若干文本片段,路径级定位返回的是一整条从入口到出口的节点链。两者的差异非常明显。假设 Agent 被问到“上次我们是怎么解决数据同步失败问题的”,普通 RAG 可能只召回“检查了防火墙策略”这一句话,但路径级定位会返回:
入口:用户报告数据同步失败 → 步骤1:检查数据源连接状态 → 步骤2:检查防火墙策略,发现端口未放行 → 步骤3:调用配置工具放行端口 → 步骤4:重跑同步任务,验证成功 出口:问题已解决这条路径包含了完整上下文,Agent 可以直接复用步骤,而不是根据一句话猜上下文。
4.2 定位过程拆解
路径级定位可以拆成三个步骤:
第一步:语义入口召回。先用当前问题生成 embedding,在节点库中做向量相似度检索,召回多个候选入口节点。入口节点就是路径的起点或路径上某一个关键节点。
第二步:路径展开。从候选入口节点出发,沿有向边向前向后遍历,扩展出若干条完整或部分路径。这里的“边”可以按路径权重剪枝,也可以设置最大跳数,比如限制单条路径最多 8 个节点,避免路径爆炸。
第三步:路径筛选与排序。将候选路径的文字序列拼接起来,交给 LLM 或轻量排序模型打分,选中最符合当前问题意图的路径。排序标准包括:语义相关性、路径完整性、路径置信度、路径新鲜度。
4.3 伪代码:路径检索
这里给出一段通用实现伪代码,不绑定具体项目。实际接入时需要按你的 Agent 框架替换图存储和向量库接口。
def retrieve_path(query_embedding, top_k_entries=10, max_hops=6, max_paths=3): # 1. 语义入口召回 candidate_nodes = vector_store.search(query_embedding, top_k=top_k_entries) # 2. 从候选节点出发做双向路径展开 candidate_paths = [] for node in candidate_nodes: paths = graph.expand_path( start_node=node.node_id, direction="both", max_hops=max_hops, edge_filter=lambda e: e.weight >= 0.3 ) candidate_paths.extend(paths) # 3. 对路径做去重和打分 deduped = deduplicate(candidate_paths) scored = score_paths_with_llm(deduped, query_embedding) # 4. 返回 Top-N 路径 return sort_by_score(scored)[:max_paths]这段代码的关键点在于:向量检索只负责“入口”,真正的召回质量取决于路径展开和路径打分。这样即使入口节点不是最优相似片段,只要它落在正确的子图上,也能通过图结构把完整路径拉出来。
4.4 多跳与路径完整度
路径级定位最大的价值在于“多跳”。普通向量检索本质上是一阶匹配,也就是问题向量和文档片段向量直接比对。而 Agent 场景里的问题往往是多阶的,比如“上次确认了 A 依赖 B,而 B 又依赖 C,最后我们用 C 的配置解决了问题”,这种关系无法在平铺片段中体现。
图结构的边允许系统做二跳、三跳甚至更多跳的扩展。跳数越多,可用信息越丰富,但也会带来两个问题:一是检索耗时会上升,二是路径可能漂移到不相关的子图。工程上的折中方案是:
- 默认跳数 4 到 6;
- 给边设置“跨场景”禁止遍历标记,避免检索跑到其他任务领域;
- 路径扩展超过上限时停止,只保留高置信度节点的扩展结果。
5. 记忆重写机制
5.1 为什么需要重写
如果记忆只能写入、不能修改,Agent 的长期行为一定会被历史错误绑架。典型的重写触发场景包括:
- 用户明确纠正:“我刚才说的不对,应该是 B 而不是 A。”
- 任务执行失败,Agent 发现历史路径存在逻辑漏洞。
- 目标变化,原路径不再适用于新目标。
- 新信息与旧记忆冲突,比如产品接口版本升级导致旧操作步骤失效。
重写的目标不是把旧记录删掉,而是生成一条“替代路径”,让后续检索优先命中新路径,同时保留旧路径作为审计和回滚依据。
5.2 重写过程的四个阶段
重写过程可以拆成四个阶段:
触发检测。系统需要判断“当前反馈是否需要对旧路径做修改”。可以通过规则触发,比如用户消息包含“不对”“纠正”“改成”等词;也可以通过 LLM 判断,让模型比较当前反馈与已检索路径是否冲突。
旧路径定位。用路径级定位找到需要修改的旧路径。这里不能只定位到单个节点,因为修改单点可能会导致整条路径逻辑断裂。比如用户纠正的是“检查顺序应该先看网络再看数据库”,如果只改一个节点,前后步骤可能无法衔接。
新路径生成。将旧路径完整文本、当前会话上下文、用户反馈一起送入 LLM,生成修订后的路径。生成时要求模型输出结构化结果,包含保留节点、修改节点、新增节点、删除节点,以及修改原因。
写回与版本化。新路径写入图存储,并与旧路径建立replaces关系。旧路径标记为“已过期”,但不过期节点仍然可以被其他路径引用。检索排序时,新路径权重提高,旧路径权重降低但不立刻删除。
5.3 伪代码:路径重写
def rewrite_path(old_path_id, user_feedback, session_context): # 1. 从图库读取旧路径 old_path = graph.get_path(old_path_id) # 2. 让 LLM 生成新路径结构 new_path_result = llm.generate_path( old_path=old_path.to_text(), feedback=user_feedback, context=session_context ) # 3. 校验和落图 new_path = graph.create_path( nodes=parse_nodes(new_path_result), edges=parse_edges(new_path_result), source="rewrite" ) # 4. 建立版本关系 graph.create_edge( from_node=new_path.end_node, to_node=old_path.start_node, edge_type="replaces", weight=0.95 ) # 5. 可选的全局偏好同步 if new_path_result.has_global_preference: graph.update_global_preference( content=new_path_result.preference_text ) return new_path.path_id5.4 重写的风险控制
重写不是越多越好。如果每条反馈都触发大规模重写,记忆库会频繁变动,检索结果也会变得不稳定。常见的风险控制策略包括:
- 版本保留:每条路径保留历史版本,新版本明显更差时可以回滚。
- 人工确认:对影响全局偏好或核心业务规则的重写,加入人工审核队列。
- 修改范围限制:一次重写尽量只修改必要的节点子集,不要重建整条路径。
- 延迟生效:新路径先进入候选池,经过多次成功复用后再提升为高置信度路径。
6. 整体工作流程
现在把前面的机制串起来,看一个完整案例。
假设用户和 Agent 进行了一场多轮调研对话,流程如下:
- 会话开始,系统从全局层读取用户偏好和历史结论,作为 Agent 的初始化知识。
- 任务执行过程中,Agent 每完成一个步骤,系统将该步骤作为事件节点写入图中,并建立与前后步骤的边。
- 任务产生关键结论,系统将结论节点聚合到场景层,更新当前会话场景。
- 后续用户提出相关问题,系统先做路径级定位,找回完整决策路径。
- 用户纠正了某个结论,系统触发重写机制,生成替代路径并标记旧路径过期。
- 再次遇到同类问题,检索优先命中新路径,历史错误不再干扰后续决策。
从工程视角看,这六个步骤对应一套 Memory API 的核心方法:初始化、添加节点、聚合层级、路径检索、路径重写、路径淘汰。无论底层用 Neo4j、NetworkX 还是自研内存图,接口语义都基本一致。
7. 实现思路与技术要点
7.1 技术选型
| 组件 | 可选方案 | 用途 |
|---|---|---|
| LLM | GPT 系列、Claude、通义千问、DeepSeek 或任何可调用的 API | 信息抽取、路径打分、重写生成 |
| 向量库 | FAISS、Milvus、Qdrant、pgvector | 节点语义检索 |
| 图存储 | Neo4j、NetworkX、Memgraph、自研邻接表 | 路径存储与遍历 |
| 缓存 | Redis | 高频路径缓存,减少重复检索 |
如果只是原型验证,NetworkX + FAISS 就够用,不需要引入重型图数据库。生产环境如果路径规模大、并发高,建议用 Neo4j 或支持图遍历的专用存储。
7.2 数据结构设计示例
from dataclasses import dataclass, field from typing import List @dataclass class MemoryNode: node_id: str level: str # global / scenario / event content: str embedding: List[float] metadata: dict = field(default_factory=dict) @dataclass class MemoryEdge: edge_id: str from_node: str to_node: str edge_type: str # next / causes / references / replaces / belongs_to weight: float = 1.0 metadata: dict = field(default_factory=dict) @dataclass class MemoryPath: path_id: str node_ids: List[str] # 按顺序排列的节点列表 confidence: float # 路径置信度 version: int = 1 replaced_by: str = None这里需要注意,MemoryPath不是图里的实体节点,而是一个视图。路径由一串节点和边组成,存储时可以单独维护路径索引表,避免每次检索都重新遍历图。
7.3 接口设计参考
如果要把这套记忆能力封装成服务,可以参考下面的接口语义。注意这是通用设计,不是某个开源项目的既定 API。
POST /api/v1/memory/node { "content": "用户不希望对比带宽费用", "level": "event", "session_id": "session_123", "parent_id": "scenario_456", "metadata": { "timestamp": "2025-01-01T12:00:00Z" } }POST /api/v1/memory/path/retrieve { "query": "上次调研的最终结论是什么?", "max_paths": 3, "max_hops": 6 }POST /api/v1/memory/path/rewrite { "path_id": "path_789", "feedback": "用户更正:应该选 B 厂商而不是 A 厂商", "session_context": "当前会话上下文摘要" }接口设计的关键是:检索和重写都接受“查询文本”或“反馈文本”,而不是要求上游系统预先构造图查询语句。这样做的好处是,Agent 主流程不需要关心记忆内部结构,只要把自然语言传给 Memory 服务即可。
7.4 LLM 抽取的稳定性处理
这套机制非常依赖 LLM 的结构化抽取能力。如果模型把节点内容抽取得忽长忽短,或者生成的路径 JSON 格式非法,整个记忆质量都会下降。工程上建议增加一层后处理:
- 强制让 LLM 输出 JSON,并在 Prompt 中给出字段 schema 示例。
- 解析失败时重试一次,重试仍失败则降级为纯文本记忆,不阻塞主流程。
- 对节点内容长度设置上下限,比如 20 到 500 字,避免节点粒度过粗或过细。
- 对重写后的路径做一致性校验,确认没有出现悬空节点引用。
8. 实验验证与效果评估方向
标题没有提供公开实验数据,所以下面给出的是评估方向,而不是结果。如果你在做论文复现或工程验证,可以从四个维度设计实验。
8.1 检索效果维度
对比“普通向量库 Top-K 检索”与“路径级定位”的效果。评估指标包括:
- 路径命中率:召回的路径是否包含当前问题真正需要的完整链条。
- 路径完整度:路径中节点缺失比例,少了中间步骤视为不完整。
- 多跳准确率:涉及二跳、三跳关系的查询,Agent 最终是否给出正确结果。
建议在多跳问答类数据集上做验证,同时准备一批“需要引用历史路径”的 Agent 任务。
8.2 记忆更新维度
重点验证重写机制是否有效。设计实验时可以模拟这些场景:
- 用户纠正历史结论,观察再次检索时是否优先返回新路径。
- 旧路径与新路径同时存在时,排序是否正确。
- 连续多次重写后,路径版本链是否清晰,检索是否稳定。
8.3 成本与延迟维度
图记忆的代价在于多跳遍历和 LLM 路径打分。需要记录:
- 单次路径检索的平均延迟和 P95 延迟;
- 单次路径检索的 Token 消耗;
- 图存储的增长速率,尤其是节点和边的数量随时间的变化;
- 重写操作的触发频率和平均耗时。
这些数据直接决定机制能不能上生产。如果路径打分需要调用大模型,单次检索延迟会明显高于纯向量检索,建议把路径打分做成轻量模型或只在候选路径数量较多时触发。
8.4 消融实验
如果写论文,建议做消融:去掉层级结构只保留单层图、去掉路径级定位只做节点召回、去掉重写机制只追加不修改。这样能验证每个模块的贡献度。从机制设计看,路径级定位对多跳任务的影响应该最明显,层级结构对大规模记忆下的延迟影响最明显,重写对长期任务稳定性影响最明显。
9. 优点、缺点与适用场景
9.1 优点
- 保留完整决策链,Agent 可以“理解”历史操作而不是只翻到一句话。
- 层级结构能支撑大规模记忆,检索范围可控。
- 路径级定位天然适合多跳任务。
- 重写机制让记忆具备演化能力,适合长周期 Agent。
9.2 缺点
- 实现复杂度高,需要同时维护向量库、图存储、路径索引。
- 依赖 LLM 抽取质量,模型不稳定会影响整条链路。
- 多跳遍历和图增长可能带来性能问题。
- 重写机制需要额外设计版本控制,否则存在记忆漂移风险。
9.3 适用场景
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 企业级长期助理 Agent | 推荐 | 需要跨天跨周复用历史经验 |
| 自动化工作流编排 | 推荐 | 工作流本质就是路径执行历史 |
| 客服知识库 | 中等 | 如果问题高度重复,平面 RAG 可能够用 |
| 单轮短对话 | 不推荐 | 引入图记忆的收益低、成本高 |
| 低延迟高并发在线问答 | 不推荐 | 路径打分和 LLM 抽取会显著增加延迟 |
10. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 检索到的路径与问题明显不相关 | 向量入口召回不准确,召回范围太小 | 检查入口节点相似度分数 | 扩大top_k_entries,或增加入口召回候选 |
| 路径缺失中间步骤 | 边权重剪枝过严,跳数上限太低 | 查看被剪枝边的权重 | 降低剪枝阈值,适当提高max_hops |
| 重写后记忆混乱 | 没有版本控制,新旧路径未建立替换关系 | 检查路径replaced_by字段 | 统一走版本化写入流程 |
| 图节点数量增长过快 | 事件节点写入过于频繁,没有聚合 | 统计节点增长速率 | 增加定期聚合任务,将事件节点合并到场景层 |
| 路径检索延迟高 | 路径展开候选集太大,或 LLM 打分过于频繁 | 查看路径展开数量和打分次数 | 先做规则过滤再打分,设置候选路径上限 |
| LLM 抽取的节点格式不稳定 | Prompt 缺少 schema 示例 | 检查模型输出日志 | 增加 schema 示例和 JSON 校验重试机制 |
11. 最佳实践与后续方向
如果你准备在自己的 Agent 项目里引入这套机制,建议按下面的顺序落地。
第一步,先做一个最小可用版本:把历史会话改成“事件节点 + 顺序边”,然后用 NetworkX 存内存图,用 FAISS 做节点向量检索。先不要引入重写机制,只验证路径级定位是否能提升多跳问答的准确率。
第二步,把重写功能加上,但限定触发条件。先只处理用户显式纠正的信息,不做主动冲突检测。这样可以把重写的风险控制在小范围内,不会因为算法误判把正确的历史路径冲掉。
第三步,增加层级聚合。当事件节点数量超过阈值后,定期调用 LLM 把同类事件聚合为场景节点,再把关键结论提升到全局层。这一步能显著降低检索延迟,也能改善长尾记忆的命中率。
最后,如果效果稳定,可以把记忆模块从主 Agent 进程中拆成独立服务,通过 API 暴露节点写入、路径检索、路径重写三个能力。这样上层 Agent 框架不关心记忆内部结构,后续扩展也方便。
后续值得探索的方向包括:让图记忆主动参与 Agent 规划,而不只是在被检索时才起作用;引入记忆压缩机制,自动淘汰低价值节点;以及在多 Agent 协作场景中共享图记忆,让多个 Agent 共同维护一张经验图。
关于这套机制,最值得先验证的就是路径级定位能否解决普通向量检索的“只找到片段、找不到链路”问题。最容易踩的坑是 LLM 抽取不稳定和图增长失控,所以第一步一定要加格式校验和版本控制。如果你正在做 Agent 长期记忆方面的选型,这套思路可以作为对比方案重点参考。