三种AI记忆架构对决:上下文图的关系优势与token效率

核心概览
- 该系统最初并非为了构建新的内存架构,而是为了解决多代理系统中跨代理决策遗忘的问题,基准测试是后续补充的验证环节。
- 多代理系统出现跨代理决策丢失的问题,根源在于纯文本会话和向量搜索都存在结构性盲区,而非仅仅是噪声干扰问题。
- 上下文图将事实以实体和关系的形式存储,而非独立文本块,因此可以回答需要结合两个独立事实的关联查询。
- 该项目并非概念验证:共测试了三种内存架构、五个脚本化场景、18个分级查询,全程确定性行为,未使用任何大语言模型调用。
- 测试数据显示:上下文图的准确率为88.9%,单查询平均token数为26.9;原始历史转储准确率为61.1%,单查询平均token数为490.9;纯向量RAG准确率为50.0%,单查询平均token数为75.9。
- 开发过程中发现了两个真实bug:陈旧事实检索问题和实体匹配缺口,相关细节已在文中说明。
触发研发的核心问题
研究人员最初开发的三代理管道在短任务中表现良好,但当对话变长,代理需要回忆过往决策时,系统就会彻底失效。
具体失效场景如下:规划代理会决定项目使用PostgreSQL作为存储技术,之后经过多轮无关对话,最终审核代理会询问当前使用的存储技术是什么。即便完整的原始会话记录都在上下文窗口中,代理依然无法可靠地给出答案。
研究人员最初认为这是大语言模型的能力局限,但后续验证发现,这本质上是内存架构的问题,根据修复方案的不同,通常会引发两种严重的问题。
向量搜索的结构性陷阱
如果使用向量搜索来修复遗忘问题,虽然可以解决噪声干扰的问题,但会立刻引入新的缺陷。向量存储只会检索与查询语义相似的文本块,无法获取事实之间的关联关系。
如果一个关键决策存储在一个文本块中,而该决策的关键依赖说明存储在另一个文本块中,无论嵌入模型的效果有多好,相似性搜索都无法将两者结合起来获取完整信息。
无论是纯文本会话还是向量搜索,都存在不同的结构性上限。研究人员没有选择猜测哪种折中方案“足够好用”,而是决定对两种方案进行量化测试。
问题的核心本质
需要明确的是,本文讨论的并非令牌压缩问题,也不是事实陈旧问题,而是结构性检索问题。有些查询只能通过结合两个独立陈述的事实才能得到答案,而无论是扩展上下文窗口还是向量索引,都没有原生机制来完成这种关联查询。这与此前讨论过的失效模式完全不同,因此需要专门的基准测试来验证。
基准测试的搭建方案
为了验证该问题,研究人员搭建了五个确定性行为的场景,包含18个分级查询,并让三种内存架构在完全相同的会话场景下进行测试。
所有测试结果均来自该基准测试的实际运行,运行环境为:
- 运行环境:Python 3.12,仅使用CPU(无需GPU支持)
- API调用:无任何大语言模型调用
- 一致性:在两台独立的机器上运行结果完全一致
完整的实现代码和测试脚本可以在公开仓库获取:https://github.com/example/context-graph-benchmark/
上下文图的核心概念
传统的扁平内存存储(无论是原始聊天记录还是向量索引)都会将每一轮对话视为独立的文本单元,检索时只会寻找与查询最匹配的单元。
而上下文图则完全改变了底层存储结构,将内存视为带有类型化关系的独立实体集合,例如:
- 认证模块 ——> 依赖于 ——> 限流模块
- 实现代理 ——> 负责分配到 ——> 认证模块
在这种模型中,检索过程不再是简单匹配关键词或语义向量,而是通过遍历这些实体之间的关系来获取信息。
这种结构性差异仅在一类特定查询中体现出优势:需要结合两个独立陈述事实的查询。例如类似“负责X所选服务的组件所属的团队是哪个?”的问题,在原始会话记录中不存在单独包含答案的文本块,答案仅存在于多个事实构成的关联路径中。扁平存储无法动态构建这种路径,而上下文图可以轻松完成遍历。
该方案的适用场景
如果你的系统运行多代理管道,且不同代理的决策需要在多轮对话后被其他代理准确检索到,那么这种方案值得开发。它专门为那些需要频繁结合多个独立陈述事实进行查询的系统,或是长会话代理系统设计——这类系统中重复发送完整历史记录的令牌成本已经成为显著的开销项。
以下场景则不适合使用该方案:单代理单轮对话任务(不存在跨代理状态丢失的问题);所有查询都是单事实查找且无需关联的场景(纯向量RAG可以以更低的工程成本实现大部分准确率);团队无法容忍额外的复杂性组件的场景(上下文图需要实体提取步骤,在基准测试中是基于规则的,生产环境则需要大语言模型调用,而扁平存储无需该步骤)。
如果你的多代理系统仅需一次交互即可完成任务,那么普通的上下文传递就足够了。该问题仅在对话变长、决策需要在创建后的多轮对话中保留时才会显现。
三种内存架构的对比
| 架构类型 | 存储内容 | 成本特性 | 擅长场景 |
|---|---|---|---|
| 原始历史转储 | 完整保留每一轮对话的原始文本 | 随对话长度增长,每次查询都需要重新发送完整会话 | 无特定优势,仅能依赖完整的上下文信息 |
| 纯向量RAG | 将每一轮对话(包括干扰项)嵌入后作为文本块存储 | 单次查询成本固定,但丢失了关系结构 | 查找语义相似的单条事实 |
| 上下文图 | 以NetworkX图的形式存储结构化三元组 | 单次查询成本低且固定 | 需要结合两个独立事实的查询 |
基准测试中未使用LLM的原因
研究人员刻意在基准测试的所有阶段都未使用大语言模型调用:无论是实体提取、查询回答还是结果评分。
如果使用大语言模型进行提取,那么基准测试的结果会同时衡量大语言模型的方差和架构本身的差异。使用确定的、基于规则的替代方案,可以确保每一次运行都得到完全相同的数值结果。
研究人员在撰写本文期间,在两台独立的机器上运行了该测试,输出结果完全一致,准确率和令牌计数都精确到整数级别。
构建不偏向上下文图的公平基准
让上下文图在基准测试中获胜最简单的方法,就是仅使用单事实查询进行测试,这毫无意义。为了保证测试的公平性,每个场景都遵循四个严格的规则:
- 干扰项数量多于事实:每个场景中包含大量诸如“听起来不错”“我会检查”“我这边没有障碍”的闲聊轮次,远多于实际的具体决策内容。
- 查询覆盖不同的间隔距离:部分查询在事实陈述后立刻提出(直接查询),部分在多轮之后提出(远距离查询),还有部分需要结合两个独立的事实(关联查询)。例如关联查询示例:“由实现代理负责的模块所依赖的组件是什么?”
- 包含故意设计的简单查询:直接的单事实查找查询,目的是让扁平架构也能获得公平的表现机会。
- 评分完全确定:基准测试使用子字符串匹配与手动编写的真实答案进行比对,而非依赖大语言模型作为评分者。
@dataclass
class Turn:
turn_id: int
turn_type: TurnType # FACT, DISTRACTOR, or QUERY
speaker: str
text: str
subject: str | None = None # structured triple, FACT turns only
predicate: str | None = None
object: str | None = None
fact_id: str | None = None
query_type: str | None = None # "direct", "distant", "join"
required_fact_ids: tuple = ()
ground_truth: str | None = None
该基准测试覆盖了五个不同领域的场景:软件规划、研究管道、事件响应、客户支持升级和数据管道。
在这五个场景中,共包含18个查询,分为三个具体类别:
- 6个直接查询:在事实陈述后立刻提出的查找请求。
- 7个远距离查询:在事实陈述多轮之后提出的查找请求。
- 5个关联查询:需要结合两个独立陈述的事实才能得到答案的查询。
架构1:原始历史转储
每一轮对话都会被追加到扁平会话记录中,每次查询时都会重新发送完整的会话记录。这正是未专门设计内存系统时的默认方案。
研究人员开发该架构作为公平的基线,它可以获取完整无遗漏的会话记录。答案提取使用关键词重叠结合轻度词干提取,从最新的对话轮次向后搜索,这与上下文塞满提示词时的近期权重倾向一致。
class RawHistoryDump:
def ingest(self, turn: Turn) -> None:
self.transcript.append(f"{turn.speaker}: {turn.text}")
def answer_query(self, query_turn: Turn) -> tuple[str, int]:
prompt = self._build_prompt(query_turn) # the ENTIRE transcript
tokens = count_tokens(prompt)
answer = self._extract_answer(query_turn)
return answer, tokens</code></pre>
该架构的成本模型与生产环境完全一致:每次查询都需要重新发送不断增长的完整会话历史。
架构2:纯向量RAG
每一轮对话(无论事实还是干扰项)都会被嵌入后作为文本块存储。真实的向量存储无法提前预知哪些轮次的内容会在后续被用到,查询时会返回相似度最高的Top-K个文本块。
研究人员使用TF-IDF而非神经嵌入API,原因与未使用大语言模型一致:TfidfVectorizer没有随机状态,因此天生具有确定性行为。同时,TF-IDF并非简单的替代方案,它是生产环境RAG中常用的稀疏检索方法,常与密集嵌入结合使用。
class VectorOnlyRAG:
def _retrieve(self, query_text: str) -> list[str]:
if not self.chunks:
return []
corpus = self.chunks + [query_text]
vectorizer = TfidfVectorizer()
matrix = vectorizer.fit_transform(corpus)
sims = cosine_similarity(matrix[-1], matrix[:-1]).flatten()
top_idx = sims.argsort()[::-1][:self.top_k]
return [self.chunks[i] for i in top_idx if sims[i] > 0]
(实际实现中会将fit_transform包装在try/except块中,以处理查询仅包含停用词的罕见边缘情况,本文为节省篇幅省略了该部分代码,完整代码可在仓库中查看。)
该架构的结构性上限非常明确:关联查询需要结合两个独立的事实,如果这两个事实分布在不同的对话轮次中,那么没有任何单个文本块会同时包含这两个信息,无论嵌入模型效果多好都无法解决这个局限。
架构3:上下文图
事实会以(主体,谓语,客体)的三元组形式写入NetworkX有向多重图中,干扰项轮次不会被存储。这是该架构相较于另外两种架构的唯一优势:在数据写入存储前就完成了过滤。
在生产环境中,这个过滤步骤需要通过大语言模型调用来完成实体提取,而在该基准测试中,由于场景已经预先标记了哪些轮次是事实,因此该步骤是确定的。研究人员仅隔离存储和检索架构本身的表现,将提取步骤作为既定假设,并未声称解决了实体提取问题。
class ContextGraph:
def ingest(self, turn: Turn) -> None:
if turn.subject is None:
return # distractors carry no structured triple; not stored
self.graph.add_node(turn.subject)
self.graph.add_node(turn.object)
self.graph.add_edge(turn.subject, turn.object,
predicate=turn.predicate, fact_id=turn.fact_id)
关联查询的遍历是该架构的核心功能,它会在图节点之间进行两跳遍历,而非寻找同时包含两个事实的单个文本块。
def _answer_join(self, query_turn, mentioned):
for entity in mentioned:
out_edges, in_edges = self._edges_touching(entity)
intermediates = [v for _, v, _ in out_edges] + [u for u, _, _ in in_edges]
for intermediate in intermediates:
further_out, _ = self._edges_touching(intermediate)
for _, target, data in further_out:
if target != entity:
# score candidates by predicate relevance
...
原始历史和向量搜索仅检索文本,而上下文图检索实体间的关系。通过遍历关联的实体,系统可以回答相似度搜索无法覆盖的多跳查询问题。
首次运行时发现的问题与修复
首次完整运行测试时,上下文图的准确率为0%。
研究人员保留了这个过程,因为大多数“我开发了X”的文章都会跳过这部分。他们本可以重写场景让测试更顺利,而非调试代码,但那样只会得到虚假的结果。因此团队选择追踪问题根源。
Bug 1:实体词汇不匹配
图节点的命名方式为Project_Alpha或AuthModule,而代理在实际查询中会使用“这个项目”或“认证模块”这样的表述。查询文本与节点名称之间的字面子字符串匹配无法找到任何结果。
这与人们批评向量搜索时提到的词汇不匹配问题完全一致,只是该问题在上下文图中出现在写入阶段而非查询阶段。
修复方案是使用一个小型的别名表,替代真实的实体链接步骤——生产环境中该步骤通常由大语言模型调用完成。使用上下文图并不能解决这个问题,只是将问题从查询时的检索阶段转移到了写入时的解析阶段,这是一项持续的工程成本,而非一次性修复。
Bug 2:无条件返回陈旧事实
这是研究人员向任何考虑在生产环境中使用该模式的人首要提醒的问题。
其中一个场景的支持工单初始优先级为“高”,在会话过程中被重新分类为“紧急”。当查询“当前的优先级是什么?”时,上下文图返回了“高”——陈旧的数值,且置信度与当前值完全相同。
问题根源非常简单:最初的ingest()实现只是添加新的边,从未删除旧的边。图中存在来自同一节点的两条HAS_PRIORITY边,遍历顺序中先出现的边会胜出,完全忽略了哪个事实是最新的。
# the bug
Ticket_4471 --HAS_PRIORITY--> "high" # stated first
Ticket_4471 --HAS_PRIORITY--> "critical" # stated later, supersedes the first
# both edges exist at once; nothing tells the graph which one is "now"
带有近期权重的扁平聊天记录搜索通常会通过向后扫描返回最新的提及内容,而没有时间模型的上下文图会以相同的结构置信度返回任意一个事实,因为图本身不会默认知道某个关系已被替换,除非显式告知。
这种失效模式比模糊搜索返回陈旧块更糟糕:上下文图看起来完全权威,但实际上完全错误。
修复方案:当新的事实重复了已存在的(主体,谓语)对时,在写入新边之前先删除旧的边。
def ingest(self, turn: Turn) -> None:
if turn.subject is None:
return
self.graph.add_node(turn.subject)
self.graph.add_node(turn.object)
stale_edges = [
(u, v, k) for u, v, k, data in self.graph.edges(keys=True, data=True)
if u == turn.subject and data.get("predicate") == turn.predicate
]
for u, v, k in stale_edges:
self.graph.remove_edge(u, v, key=k)
self.graph.add_edge(turn.subject, turn.object,
predicate=turn.predicate, fact_id=turn.fact_id)</code></pre>
如果要在生产环境中使用类似方案,处理事实的替换并非可选步骤,而是构建可靠内存层与构建重大风险的分界线。
最终基准测试结果
该基准测试覆盖五个场景、18个查询,全程确定性行为,在两台独立机器上运行结果完全一致。
| 架构类型 | 准确率 | 单查询平均token数 | 直接查询准确率 | 远距离查询准确率 | 关联查询准确率 |
|---|---|---|---|---|---|
| 原始历史转储 | 61.1% | 490.9 | 66.7% | 71.4% | 40.0% |
| 纯向量RAG | 50.0% | 75.9 | 66.7% | 57.1% | 20.0% |
| 上下文图 | 88.9% | 26.9 | 100% | 85.7% | 80.0% |
上下文图在准确率上胜出,且单查询平均token数仅为原始历史转储的约1/18,这并非权衡,而是在两个维度上都取得了优势。
纯向量RAG的令牌成本也较低,但这并非上下文图的主要差异点。两种架构都仅检索固定数量的条目,因此无论对话长度如何,成本都保持稳定。上下文图与向量RAG的核心差异在于关联查询的准确率:80% vs 20%。这个差距正是支持上下文图的结构性论据:向量相似度搜索本身没有原生机制来结合两个独立陈述的事实。
原始历史转储的准确率达到了61.1%,超出了研究人员的预期,这得益于完整无损的会话记录和良好的关键词匹配能力,在单事实查找场景中表现尚可,但在关联查询场景中(准确率仅40%)同样存在结构性缺陷,只是令牌成本高得多。
有一个限制被刻意保留:数据管道场景中的两个查询失败,因为它们使用描述而非名称来指代实体——例如“当前存在异常的数据集”而非直接命名为Upstream_Orders。修复这个问题需要对描述性子句进行真正的语义理解,而非简单的别名匹配。扩展别名表以覆盖测试查询会导致基准测试过拟合,而非反映真实的限制,因此团队保留了这个问题。如果生产环境中的查询更多使用描述性引用,则需要预算用于大语言模型的解析步骤,而非不断增长的静态别名表。
令牌成本随对话长度的变化规律
研究人员最初假设原始历史转储的令牌成本随对话长度呈O(N²)增长,但团队选择实际测量而非直接假设,因为向检查该结论的受众传递不准确的复杂度主张会快速损失可信度。
测试设置:首先陈述一个事实,然后添加数量递增的 filler 轮次(从10到800),最后发起一个查询获取该事实。该设置隔离了单查询令牌成本作为对话长度的纯函数,且信息内容完全固定。
| filler轮次数量 | 原始历史转储token数 | 向量RAG token数 | 上下文图token数 |
|---|---|---|---|
| 10 | 157 | 54 | 23 |
| 50 | 659 | 54 | 23 |
| 100 | 1,287 | 54 | 23 |
| 200 | 2,542 | 54 | 23 |
| 400 | 5,052 | 54 | 23 |
| 800 | 10,072 | 54 | 23 |
当对话长度从10轮增长到800轮(增长80倍),原始历史转储的token数增长了64.15倍。而向量RAG和上下文图的token数均保持不变,增长倍数为1.00x。
原始历史转储的单查询token成本为O(N),即与对话长度呈线性关系,最终收敛到约每个filler轮次12.6个token。并非最初假设的O(N²)。只有当计算多查询会话的总成本时,O(N²)的说法才成立:Q次查询,每次查询都针对不断线性增长的会话记录,总成本约为O(N·Q)。这才是真实的成本模型,比“每个查询成本为O(N²)”更精确。
向量RAG和上下文图的单查询成本均为O(1),因为无论对话长度如何,两种架构都仅检索固定数量的条目。
生产环境部署前的注意事项
在任何人将该模式复制到真实应用之前,有几个关键点需要明确说明。
- 延迟表现:实际上向量RAG是三种架构中最慢的,而非上下文图。向量RAG每次查询都会重新在整个语料库上拟合TF-IDF,而非维护增量索引。在五个场景中,上下文图的查询回答平均耗时为0.050ms,而向量RAG为1.764ms。这个差距在真实部署中会缩小,因为可以缓存向量化器而非每次重新拟合——基准测试测量的是默认行为,而非最优的工程实现。上下文图偶尔的1.9ms延迟仅来自关联查询遍历多个候选路径的过程。
- 别名表的实际作用:让“认证模块”解析为AuthModule的实体别名表,是真实实体链接的硬编码替代方案。在生产环境中,该步骤需要大语言模型调用。基准测试之所以确定,是因为研究人员硬编码了预期的别名,但这并不意味着词汇不匹配问题已经解决了任意查询表述的情况。这是一项持续的实际成本,研究人员并未隐瞒。
- 令牌计数估算:研究人员使用了约每4个字符对应1个token的启发式方法,而非tiktoken,因为tiktoken首次使用时会从远程URL下载BPE等级文件,这会引入隐藏的网络依赖,而基准测试刻意避免了这种情况。该启发式方法在三种架构中应用完全一致,因此不会影响架构之间的对比,但绝对token数值为近似值。
- 未测试的场景:本次基准测试中的干扰项均为通用闲聊,例如“我这边没有障碍”“听起来不错”。真实生产环境中的干扰项可能与实际事实主题高度相关。研究人员预计所有三种架构在对抗性噪声下准确率都会下降,但未对此进行测量,因此无法保证当前的领先优势依然存在。
- 生产环境缺失的功能:真实的实体提取(ingest()接口已经接受结构化三元组,因此替换为大语言模型提取器是一个可控的更改)、增量向量索引、针对长会话的图修剪(避免无限增长的实体集合)、持久化存储。仓库中包含了NetworkX到Neo4j的导出路径,用于需要持久性和并发多代理写入的场景,但这是可选步骤,并非性能升级。选择该迁移的原因是事务性保证和并发支持,而非原始查询速度。
测试数据的核心启示
所有的结果都不需要使用更大的模型或更长的上下文窗口,每一个结果都来自信息表示方式的改变,而非向提示中塞入更多数据。
如果只能从本文中记住一个数字,那就是关联查询的准确率差距:80% vs 20-40%。这才是支持结构化内存的核心论据,而非令牌成本的节省。
虽然令牌成本的节省是真实且可测量的,但这只是次要优势。在本次基准测试中,需要结合会话两个不同部分的两个事实的查询,是上下文图架构展现最大优势的场景。该差距在所有五个场景中都保持一致,而非仅在对上下文图友好的场景中。
该项目的完整内容——五个场景、三种架构、锁定测试结果的回归测试套件、以及Neo4j导出路径——均可在公开仓库获取。
塔猴是一个专注于为用户提供系统学习、内容创作与商业连接的AIGC综合服务平台,致力于为每一位AI探索者打造理想的创作、成长家园。在塔猴,你不仅可以学习众多AIGC类实战课程,获得与时俱进的AIGC技能和视野,还有机会获得长期商业合作和接单机会!点击进入:https://www.tahou.com/
AI生成内容提示:本文由人工智能辅助创作,内容仅供参考,不代表平台观点。请注意核实信息的准确性,并理性判断。




