TL;DR 太长不看(TL;DR)
- I wasn’t trying to build a new memory architecture. I was trying to understand why one agent kept forgetting decisions made by another. The benchmark came later.我原本并不是想构建一种新的记忆架构。我是想弄清楚为什么一个智能体总是忘记另一个智能体做出的决定。基准测试是后来才做的。
- Multi-agent systems lose cross-agent decisions because flat transcripts and vector search both have a structural blind spot — not just a noise problem.多智能体系统会丢失跨智能体的决策,因为扁平化的聊天记录和向量搜索都存在结构性盲区——不仅仅是噪声问题。
- A context graph stores facts as entities and relationships instead of text chunks, so it can answer questions that need two facts combined.上下文图将事实存储为实体和关系,而非文本块,因此它能够回答需要结合两个事实的问题。
- This is not a concept. Three memory architectures, five scripted scenarios, 18 graded queries, fully deterministic, zero LLM calls.这不是一个概念。三种记忆架构、五个脚本化场景、18个评分查询、完全确定性、零LLM调用。
- Context graph: 88.9% accuracy at 26.9 tokens/query. Raw history dump: 61.1% accuracy at 490.9 tokens/query. Vector-only RAG: 50.0% accuracy at 75.9 tokens/query.上下文图:88.9% 准确率,每次查询26.9个token。原始历史记录:61.1% 准确率,每次查询490.9个token。纯向量RAG:50.0% 准确率,每次查询75.9个token。
- I found two real bugs building this — stale-fact retrieval and an entity-matching gap. Both are in the article.我在构建过程中发现了两个真正的bug——陈旧事实检索和实体匹配缺失。两者都在文章中。
The Problem That Made Me Build This促使我构建这个的问题
I built a three-agent pipeline that worked great for short tasks. But the moment the conversation dragged on and an agent needed to recall a past decision, the whole thing fell apart. 我构建了一个三智能体流水线,处理短任务时表现很好。但一旦对话拉长,某个智能体需要回忆过去的决策时,整个系统就崩溃了。
Here is exactly how it broke: Agent_Planner would decide the project should use PostgreSQL. Then, twenty turns of “sounds good” and “I’ll get to it” would pass. Eventually, Agent_Reviewer would pipe up and ask what storage technology we were using. Even with the entire raw transcript sitting right there in the context window, the agent couldn’t answer reliably.它刚好就是这样崩溃的:Agent_Planner决定项目应该使用PostgreSQL。然后经过二十轮“听起来不错”和“我会去做的”之后,Agent_Reviewer最终会插嘴问我们使用的是什么存储技术。即使完整的原始聊天记录就放在上下文窗口里,智能体也无法可靠地回答。
I was running this pipeline locally as a side project for EmiTechLogic just to see how far I could push multi-agent coordination before it hit a wall. Turns out, it didn’t take very long.我在本地运行这个流水线,作为EmiTechLogic的一个副业项目,只是想看看在多智能体协调碰壁之前我能把它推到多远。结果发现,这并没有花很长时间。
Initially, I assumed this was just a model limitation. It isn’t. It is a memory architecture problem that usually triggers one of two massive headaches depending on how you try to fix it.起初,我以为这只是模型的限制。事实并非如此。这是一个记忆架构问题,根据你尝试修复的方式不同,通常会引发两种令人头疼的问题之一。
The Alternative Fix: Vector Search and the Relational Trap替代方案:向量搜索与关系陷阱
If you switch to vector search, you fix the noise problem but immediately create a different one. A vector store retrieves chunks that look similar to your query; it doesn’t retrieve relationships between facts.如果你转向向量搜索,你解决了噪声问题,但立刻产生了另一个问题。向量存储检索的是与查询看起来相似的文本块;它无法检索事实之间的关系。
If a key decision lives in one chunk and a critical dependency note about that decision lives in another, a similarity search has no way to combine them—no matter how good your embedding model is.如果一个关键决策存在于一个文本块中,而关于该决策的关键依赖说明存在于另一个文本块中,相似性搜索无法将它们结合起来——无论你的嵌入模型有多好。
Both approaches hit different structural ceilings. Instead of guessing which compromise was “good enough,” I decided to measure them both.两种方法都遇到了不同的结构性天花板。与其猜测哪种折中方案“足够好”,我决定对它们都进行测量。
What This Problem Actually Is这个问题到底是什么
To be clear about what this article is not: this isn’t a token-compression problem, and it’s not a staleness problem. It’s a structural retrieval problem. Some questions can only be answered by combining two separately-stated facts, and neither a growing context window nor a vector index has a mechanism to do that. That is a completely different failure mode than the ones I’ve written about before, and it needed a different benchmark.为了明确这篇文章不是什么:这不是一个token压缩问题,也不是一个陈旧性问题。这是一个结构性检索问题。有些问题只能通过结合两个分别陈述的事实来回答,而不断增长的上下文窗口和向量索引都没有实现这一点的机制。这与我之前写过的失败模式完全不同,因此需要一个不同的基准测试。
The Test Setup测试设置
To test this, I built five deterministic scenarios containing 18 graded queries and ran all three memory architectures against the exact same conversations.为了测试这一点,我构建了五个确定性场景,包含18个评分查询,并在完全相同的对话上运行了三种记忆架构。
All the results below come from real runs of that benchmark using a localized setup:下面的所有结果都来自于使用本地化设置对该基准测试的真实运行:
- Environment: Python 3.12, CPU-only (no GPU needed)环境:Python 3.12,仅CPU(无需GPU)
- API Calls: ZeroAPI调用:零
- Consistency: Reproduced identically across two separate machines一致性:在两台不同的机器上重现结果完全一致
Code Repo: You can find the complete implementation and run the tests yourself here: https://github.com/Emmimal/context-graph-benchmark/代码仓库:你可以在这里找到完整的实现并自行运行测试:https://github.com/Emmimal/context-graph-benchmark/
What “Context Graph” Means Here本文中“上下文图”的含义
A flat memory store (whether it is a raw chat transcript or a vector index) treats every single turn as an independent unit of text. To retrieve something, you just find the unit that best matches your query.扁平化的记忆存储(无论是原始聊天记录还是向量索引)将每一轮对话都视为独立的文本单元。要检索某些内容,你只需要找到最匹配查询的单元。
A context graph changes the underlying structure entirely. It treats memory as distinct entities with typed relationships connecting them:上下文图彻底改变了底层结构。它将记忆视为由带类型的关系连接起来的不同实体:
AuthModule—–>DEPENDS_ON—–>RateLimiterAuthModule —–> DEPENDS_ON —–> RateLimiterAgent_Implementer—–>ASSIGNED_TO—–>AuthModuleAgent_Implementer —–> ASSIGNED_TO —–> AuthModule
Retrieval in this model means traversing these relationships instead of just matching keywords or semantic vectors.在这种模型中,检索意味着遍历这些关系,而不仅仅是匹配关键词或语义向量。
That structural difference only matters for one specific class of questions: anything that requires you to combine two separately-stated facts.这种结构性差异只对一类特定的问题重要:任何需要你将两个分别陈述的事实结合起来的问题。
Consider a question like: “Which team owns the component that depends on the service that X chose?”考虑这样一个问题:“哪个团队拥有依赖于X所选择服务的组件?”
There is no single answer chunk sitting anywhere in the raw conversation history. The answer does not exist as a block of text. It only exists as a path through multiple facts. A flat store cannot construct that path on the fly. A graph walks right through it.在原始对话历史中,没有任何地方存在一个包含答案的单一文本块。答案并不作为文本块存在。它只存在于跨越多个事实的路径中。扁平化存储无法即时构建这条路径。而图可以直接遍历它。
Who This Is For本文适合谁
This approach is worth building if you run multi-agent pipelines where one agent’s decision must be correctly retrieved by a different agent many turns later. It is built for systems where questions routinely require combining two or more separately-stated facts, or any long-running agent conversation where the token cost of re-sending history is becoming a real line item.如果你运行的多智能体流水线中,一个智能体的决策必须在很多轮之后被另一个智能体正确检索,那么这种方法值得构建。它适用于那些问题通常需要结合两个或更多分别陈述的事实,或者任何重新发送历史记录的token成本正在成为实际开支项的长期运行的智能体对话。
You should skip it for single-agent, single-turn tasks because there is no cross-agent state to lose. Skip it if your queries are always single-fact lookups with no joins. Vector RAG gets you most of the accuracy there at a fraction of the engineering cost. Finally, skip it if your team has no tolerance for an extra moving part. A graph needs an extraction step (which is rule-based in this benchmark, but requires an LLM call in production) that a flat store avoids.对于单智能体、单轮任务,你应该跳过它,因为没有跨智能体的状态会丢失。如果你的查询总是单事实查找且无需关联,也请跳过它。向量RAG在这种情况下能以极低的工程成本获得大部分准确率。最后,如果你的团队无法接受额外的一个动态组件,也请跳过它。图需要一个提取步骤(在这个基准测试中是基于规则的,但在生产中需要调用LLM),而扁平化存储则不需要。
If your multi-agent system finishes its work in a single exchange, plain context passing works fine. This problem shows up specifically when conversations run long and decisions need to survive past the turn they were made in.如果你的多智能体系统在单次交互中就能完成工作,那么简单的上下文传递就能很好地工作。这个问题只在对话持续很长时间、且决策需要在其产生的轮次之后继续存在时才会出现。
The Three Architectures三种架构
| Architecture | What it stores | What it costs | What it’s good at |
|---|---|---|---|
| Raw History Dump | Every turn, verbatim | Grows with conversation length, resent every query | Nothing it doesn’t get for free from having everything |
| Vector-Only RAG | Every turn, embedded (TF-IDF) | Flat per query, loses relational structure | Finding semantically similar single facts |
| Context Graph | Structured triples in a NetworkX graph | Flat and small per query | Questions that need two facts combined |
Why There Are No LLM Calls in the Benchmark为什么基准测试中没有LLM调用
I purposely left out LLM calls from every stage of this benchmark: no LLMs for extraction, none for query answering, and none for grading.我有意在这个基准测试的每个阶段都排除了LLM调用:没有用于提取的LLM,没有用于回答查询的LLM,也没有用于评分的LLM。
If a real LLM handled the extraction, the benchmark would measure LLM variance as much as actual architectural differences. Using deterministic, rule-based stand-ins ensures that every single run produces the exact same numbers.如果由真正的LLM来处理提取,那么基准测试测出的将是LLM的差异与真实架构差异的混合结果。使用确定性的、基于规则的替代方案可以确保每次运行都产生完全相同的数字。
I ran this test independently on two different machines while writing this piece. The output matched byte-for-byte, maintaining accuracy to four decimal places and token counts down to the exact integer.我在撰写这篇文章时,在两台不同的机器上独立运行了这个测试。输出结果逐字节匹配,准确率保持到小数点后四位,token数量保持到精确的整数。
Building a Benchmark That Doesn’t Secretly Favor the Graph构建一个不会暗中偏袒图的基准测试
The easiest way to make a graph win a benchmark is to only ask it clean, single-fact questions. That proves nothing. To keep the testing fair, every scenario follows four strict rules:让图在基准测试中获胜的最简单方法,就是只问它干净的、单事实问题。那什么也证明不了。为了保持测试的公平性,每个场景都遵循四条严格的规则:
- Distractors outnumber facts: Every scenario contains far more “sounds good,” “I’ll check that,” and “no blockers on my end” turns than actual concrete decisions.干扰信息多于事实:每个场景中包含的“听起来不错”、“我检查一下”和“我这边没有阻碍”这样的轮次要远远多于实际的明确决策。
- Queries span physical distance: Some queries are asked right after a fact is stated (direct), some are asked many turns later (distant), and some require stitching two separate facts together (join). An example of a join query is: “Which component does the module owned by Agent_Implementer depend on?”查询跨越物理距离:有些查询在事实陈述后立即提出(直接),有些在很多轮之后才提出(远距离),有些则需要将两个独立的事实拼接在一起(关联)。关联查询的一个例子是:“Agent_Implementer拥有的模块依赖于哪个组件?”
- Some queries are easy on purpose: Direct, single-fact lookups are included specifically to give the flat architectures a fair shot.有些查询是故意设置得简单的:特意包含直接的单事实查找,是为了给扁平化架构一个公平的机会。
- Grading is completely deterministic: The benchmark uses substring matching against a hand-written ground truth rather than relying on an LLM judge.评分是完全确定性的:基准测试使用与手写 ground truth 的字符串子串匹配,而非依赖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
The benchmark covers five distinct scenarios across different domains: software planning, a research pipeline, incident response, customer support escalation, and a data pipeline.基准测试涵盖五个不同领域的不同场景:软件规划、研究流水线、事件响应、客户支持升级和数据流水线。
Across these five setups, there are 18 total queries split into three specific categories:在这五个设置中,共有18个查询,分为三个特定类别:
- 6 Direct queries: Lookups asked immediately after the fact is stated.6个直接查询:在事实陈述后立即提出的查找。
- 7 Distant queries: Lookups asked many turns after the fact is stated.7个远距离查询:在事实陈述很多轮之后才提出的查找。
- 5 Join queries: Questions that require combining two separately-stated facts to get the answer.5个关联查询:需要结合两个分别陈述的事实才能得到答案的问题。
Architecture 1: Raw History Dump架构1:原始历史记录
Every single turn gets appended to a flat transcript, and the entire transcript gets resent on every query. This is exactly what you get by default when you do not design a memory system on purpose.每一轮对话都被追加到一个扁平的聊天记录中,并且每次查询都会重新发送整个记录。这就是当你没有专门设计记忆系统时,默认得到的东西。
I built this to serve as a genuinely fair baseline. It gets the full, perfect transcript with nothing hidden from it. The answer extraction uses keyword overlap with light stemming, searched from the most recent turn backward. This setup closely mirrors how a context-stuffed prompt tends to weight recency anyway.我构建这个是为了作为一个真正公平的基线。它获得了完整、完美的记录,没有任何信息被隐藏。答案提取使用关键词重叠和轻度词干提取,从最近的一轮开始向后搜索。这种设置与塞满上下文的提示词倾向于重视近因效应的方式非常相似。
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
The cost model matches exactly what you see in production: every query resends the entire growing conversation history.成本模型与你在生产中看到的完全一致:每次查询都会重新发送不断增长的全部对话历史。
Architecture 2: Vector-Only RAG架构2:纯向量RAG
Every turn, fact and distractor alike, gets embedded and stored as a chunk. A real vector store does not know in advance which turns will matter later. On a query, the top-K most similar chunks are retrieved.每一轮对话,无论是事实还是干扰信息,都会被嵌入并存储为一个文本块。真正的向量存储无法提前知道哪些轮次之后会重要。查询时,检索最相似的top-K个文本块。
I used TF-IDF instead of a neural embedding API for the same reason I avoided LLM calls elsewhere. TfidfVectorizer has no random state, making it deterministic by construction. It is also not a toy stand-in. TF-IDF is a real sparse-retrieval method used in production RAG, often paired with dense embeddings in a hybrid setup.我使用TF-IDF而不是神经嵌入API,原因与其他地方避免LLM调用相同。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]
(The actual implementation wraps fit_transform in a try/except block to handle the rare edge case of a query containing only stop words. I skipped that here for space, but it is in the repository.)(实际实现将fit_transform包装在try/except块中,以处理查询仅包含停用词的罕见边界情况。我这里为了节省篇幅省略了,但它在仓库里。)
The structural ceiling remains clear: a join query requires combining two distinct facts. When those facts are stated across two different turns, no single chunk contains both pieces of information. No embedding model can fix that limitation on its own.结构性天花板依然明确:关联查询需要结合两个不同的事实。当这些事实在两个不同的轮次中陈述时,没有一个单独的文本块包含这两段信息。没有任何嵌入模型能独自解决这一限制。
Architecture 3: The Context Graph架构3:上下文图
Facts get written as (subject, predicate, object) triples into a NetworkX directed multigraph. Distractor turns never get written at all. This is the one place this architecture gets an advantage the other two do not: filtering data before it ever hits storage.事实被写成(主语、谓语、宾语)三元组,存入一个NetworkX有向多重图。干扰轮次完全不会被写入。这是该架构相对于另外两个架构获得优势的一个地方:在数据进入存储之前进行过滤。
In production, that filtering step is an LLM call performing entity extraction. In this benchmark, it is deterministic because the scenario setup already tags which turns are facts. I am isolating exactly what the storage and retrieval architecture does on its own, with extraction held constant as a stated assumption. I am not claiming to have solved extraction for free.在生产环境中,这个过滤步骤是一个执行实体提取的LLM调用。在这个基准测试中,它是确定性的,因为场景设置已经标记了哪些轮次是事实。我正在精确地隔离存储和检索架构自身的作用,将提取视为恒定的既定假设。我并没有声称免费解决了提取问题。
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)
The join-query traversal is the part doing the real work. It performs a two-hop walk across the graph nodes instead of searching for a single text chunk that happens to contain both facts.关联查询的遍历是实际工作的部分。它在图节点上执行两跳遍历,而不是搜索一个刚好包含两个事实的单一文本块。
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
...
Here’s the difference in search space across all three:以下是三种架构在搜索空间上的区别:

What Actually Happened When I First Ran It我第一次运行它时实际发生了什么
The first full run, with all three architectures built, scored the context graph at 0% accuracy.第一次完整运行,在三种架构都构建完成后,上下文图的准确率为0%。
I’m including this because it’s the part most “I built X” posts skip. I could have rewritten the scenarios to be friendlier instead of debugging the code. That would have given me a fake result. I traced it instead.我把它写进来,是因为这是大多数“我构建了X”的文章会跳过的部分。我本可以重写场景让它们更友好,而不是调试代码。那样会给我一个假结果。相反,我追踪了它。
Bug 1: Entity Vocabulary MismatchBug 1:实体词汇不匹配
Graph nodes were named things like Project_Alpha or AuthModule. The queries, written the way an agent would actually phrase them, said “this project” or “the authentication module.” A literal substring match between the query text and the node name found absolutely nothing.图节点的名称类似于Project_Alpha或AuthModule。而查询是按照智能体实际会使用的措辞写的,说的是“这个项目”或“认证模块”。在查询文本和节点名称之间进行字面子串匹配,结果什么也没找到。
This is the exact same vocabulary-mismatch problem people criticize vector search for. It just hits the graph at write time instead of query time.这正是人们批评向量搜索的词汇不匹配问题。它只是从查询时转移到了写入时影响图。
The fix was a small alias table standing in for a real entity-linking step, which would usually be handled by an LLM call in production. Using a graph does not get you out of this problem. It simply moves the problem from query-time retrieval to write-time resolution. That is an ongoing engineering cost, not a one-time fix.修复方案是一个小型别名表,替代了真正的实体链接步骤,这在生产中通常由LLM调用来处理。使用图并不能让你摆脱这个问题。它只是把问题从查询时检索转移到了写入时解析。这是一个持续的工程成本,而非一次性修复。
Bug 2: Returning Stale Facts With Full ConfidenceBug 2:以完全置信度返回陈旧事实
This is the exact issue I would flag first to anyone shipping this pattern in a production environment.这是我向任何要在生产环境中上线这种模式的人首先会指出的问题。
One scenario features a support ticket that starts at a priority level of “high” and gets reclassified to “critical” mid-conversation. When querying “what is the current priority?”, the graph returned “high”—the stale value, with the exact same confidence it would have given the current one.一个场景中,有一张支持工单,一开始优先级为“high”,在对话中途被重新分类为“critical”。当查询“当前优先级是什么?”时,图返回的是“high”——这个陈旧的值,而它的置信度与返回当前值时完全相同。
The cause was simple: my first ingest() implementation just added every new edge and never removed the old one. The graph held two HAS_PRIORITY edges originating from the same node. Whichever edge happened to be visited first in the iteration order won the lookup, completely ignoring which fact was actually current.原因很简单:我最初的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"
A flat chat dump searched with recency bias tends to surface the newer mention just by scanning backward. In contrast, a graph with no time model hands back either fact with equal structural confidence because graphs do not natively know a relationship has been replaced unless you explicitly tell them.带有近因偏见的扁平化聊天记录通过向后扫描,往往能将较新的提及浮现出来。相比之下,一个没有时间模型的图会以同等的结构置信度返回任意一个事实,因为图本身不知道一条关系已被替换,除非你明确告诉它。
That failure mode is worse than a fuzzy search returning a stale chunk. The graph looks completely authoritative even when it is completely wrong.这种失败模式比模糊搜索返回一个陈旧的文本块更糟糕。图即使在完全错误的时候也看起来完全权威。
The fix: when a new fact restates an existing (subject, predicate) pair, the old edge gets dropped before the new one is written.修复方案:当一个新事实重述了一个现有的(主语,谓语)对时,在写入新边之前先删除旧边。
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)
If you are shipping anything like this, handling fact supersession is not optional. It is the exact line between building a reliable memory layer and building a major liability.如果你要上线任何类似的东西,处理事实更替不是可选的。这是构建一个可靠的记忆层与构建一个重大隐患之间的确切分界线。
Final Benchmark Results最终基准测试结果
Five scenarios, 18 queries, fully deterministic, reproduced identically on two separate machines.五个场景,18个查询,完全确定性,在两台不同的机器上结果完全一致。
| Architecture | Accuracy | Avg tokens/query | Direct | Distant | Join |
|---|---|---|---|---|---|
| Raw History Dump | 61.1% | 490.9 | 66.7% | 71.4% | 40.0% |
| Vector-Only RAG | 50.0% | 75.9 | 66.7% | 57.1% | 20.0% |
| Context Graph | 88.9% | 26.9 | 100% | 85.7% | 80.0% |
The context graph wins on accuracy and uses about 18x fewer tokens per query than the raw dump. That is not a tradeoff—it is a win on both axes.上下文图在准确率上胜出,并且每次查询使用的token比原始记录少了约18倍。这不是一种权衡——而是在两个维度上都赢了。
Vector RAG’s token cost is also low and isn’t the graph’s main differentiator. Both architectures retrieve a bounded number of items, so both stay cheap regardless of conversation length. What separates the graph from vector RAG is the join column: 80% versus 20%. That gap is the structural argument for a graph—vector similarity has no native way to combine two separately-stated facts.向量RAG的token成本也很低,并不是图的主要区分点。两种架构都检索有限数量的条目,因此无论对话多长,两者都能保持低成本。图与向量RAG的区别在于关联查询:80% 对 20%。这个差距是支持图的结构论据——向量相似性没有原生方式来结合两个分别陈述的事实。
The raw dump’s accuracy came in higher than I expected at 61.1%, and it earns that. A perfect, lossless transcript with decent keyword matching does fine on single-fact lookups. It falls apart specifically on joins (40%) for the same structural reason as vector RAG, just with a much bigger token bill.原始记录的准确率比我预期的高,达到了61.1%,这是应得的。一份完美、无损的记录加上不错的关键词匹配,在单事实查找上表现不错。它在关联查询上崩溃(40%),原因与向量RAG的结构性缺陷相同,只是token成本要高得多。
One limitation was left in on purpose: two queries in the data-pipeline scenario fail because they refer to an entity by description rather than name—”the dataset that currently has an anomaly” instead of naming Upstream_Orders directly. Fixing that requires real semantic understanding of a descriptive clause, not simple alias matching. Extending the alias table to cover my own test queries would mean overfitting the benchmark rather than representing a real limitation, so it stays broken. If your production queries lean toward descriptive references, budget for an LLM-based resolution step instead of an ever-growing static alias table.有一个限制是故意的:数据流水线场景中有两个查询失败了,因为它们通过描述而非名称来指代实体——“当前存在异常的数据集”,而不是直接命名Upstream_Orders。修复这个问题需要对描述性从句进行真正的语义理解,而不是简单的别名匹配。将别名表扩展以覆盖我自己的测试查询,意味着对基准测试过拟合,而非代表真正的限制,因此我让它保持失败状态。如果你生产环境中的查询倾向于描述性指代,请为基于LLM的解析步骤做预算,而不是不断增长的静态别名表。
How Token Cost Scales With Conversation Length Token成本如何随对话长度增长
My working assumption going in was that raw-dump token cost scales O(N^2) as conversations grow. I measured it instead of assuming it, because shipping an imprecise complexity claim to an audience that checks it is a fast way to lose credibility.我最初的假设是,随着对话增长,原始记录的token成本按O(N^2)增长。我测量了它,而不是假设它,因为向一个会核实的受众提出不精确的复杂度主张,是快速失去可信度的方法。
The setup: one fact stated once, followed by a growing number of filler turns (ranging from 10 up to 800), followed by a single query asking for that fact. This isolates per-query token cost as a pure function of conversation length, with information content held completely fixed.设置:一次陈述一个事实,然后是一系列逐步增加的填充轮次(从10到800),最后是一个针对该事实的查询。这将每次查询的token成本隔离为对话长度的纯函数,而信息内容保持完全固定。
| Filler turns | Raw Dump tokens | Vector RAG tokens | Context Graph tokens |
|---|---|---|---|
| 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 |
When the conversation length grew 80x (from 10 to 800 turns), the raw dump’s token count grew 64.15x. Meanwhile, vector RAG and the context graph both grew 1.00x—completely flat.当对话长度增长80倍(从10轮到800轮)时,原始记录的token数量增长了64.15倍。与此同时,向量RAG和上下文图都增长了1.00倍——完全持平。
The raw dump’s tokens-per-query is O(N), which is linear in conversation length, converging to about 12.6 tokens per filler turn. It is not quadratic. The O(N^2) story only becomes accurate if you sum the cost across an entire multi-query conversation: Q queries, each run against a transcript that has grown linearly, lands around O(N.Q) total cost. That is the real number, just a more precise one than “each query costs O(N^2).”原始记录每次查询的token数是O(N),即与对话长度呈线性关系,收敛到每轮填充约12.6个token。它不是二次的。O(N^2)的说法只有在汇总整个多查询对话的成本时才准确:Q次查询,每次都针对一个线性增长的记录运行,总成本约为O(N·Q)。这才是真实的数字,只是比“每次查询成本为O(N^2)”更精确。
Vector RAG and the context graph both hold flat at O(1) per query because both architectures only ever pull a bounded number of items regardless of how long the conversation gets.向量RAG和上下文图都保持在每次查询O(1)的平坦状态,因为无论对话多长,两种架构都只提取有限数量的条目。

What I’d Flag Before Taking This to Production在将其投入生产之前我会提醒的事项
A few things are worth being direct about before anyone copies this pattern into a real application.在任何人将这种模式复制到真实应用之前,有几件事值得直说。
On latency: Vector RAG is actually the slowest architecture here, not the graph. It refits TF-IDF over the entire corpus on every query call rather than maintaining an incremental index. Averaged across all five scenarios, context graph query answering came in at 0.050ms versus Vector RAG’s 1.764ms.关于延迟:向量RAG实际上是这里最慢的架构,而不是图。它在每次查询调用时都会在整个语料库上重新拟合TF-IDF,而不是维护增量索引。在所有五个场景中取平均值,上下文图查询回答耗时0.050毫秒,而向量RAG为1.764毫秒。
That gap closes in a real deployment where you would cache the vectorizer instead of refitting from scratch—the benchmark measured default behavior, not best-case engineered versions. The graph’s occasional spike to 1.9ms comes entirely from join queries walking multiple candidate paths before scoring.在真实部署中,这个差距会缩小,因为你会缓存向量化器而不是从头重新拟合——基准测试测量的是默认行为,而非最佳工程版本。图的偶尔峰值达到1.9毫秒,完全来自于关联查询在评分前遍历多个候选路径。
On what the alias table is actually doing: The entity alias table that lets “the authentication module” resolve to AuthModule is a hardcoded stand-in for real entity linking. In production, that step is an LLM call. The benchmark is deterministic because I hardcoded the aliases I anticipated—it does not mean the vocabulary-mismatch problem is solved for arbitrary query phrasing. It is a real ongoing cost that I am flagging, not hiding.关于别名表实际在做什么:让“the authentication module”解析到AuthModule的实体别名表,是真实实体链接的硬编码替代品。在生产环境中,这一步是一个LLM调用。基准测试是确定性的,因为我硬编码了我预期的别名——这并不意味着词汇不匹配问题对于任意查询措辞都已解决。这是我正在指出而非隐瞒的真实持续成本。
On token estimation: I used a ~4-characters-per-token heuristic instead of tiktoken, because tiktoken downloads its BPE rank file from a remote URL on first use—a hidden network dependency in a benchmark built to have none. The heuristic is applied identically across all three architectures, so it cannot bias the comparison between them, but the absolute token numbers are approximations.关于token估算:我使用了每个token约4个字符的启发式方法,而不是tiktoken,因为tiktoken在首次使用时会从远程URL下载其BPE排名文件——这在一个旨在没有任何网络依赖的基准测试中是一个隐藏的网络依赖。该启发式方法在三种架构上的应用方式完全相同,因此不会对它们之间的比较产生偏倚,但绝对token数量是近似值。
On what this benchmark did not test: Distractor turns here are generic chatter—”no blockers on my end,” “sounds good.” Real production noise is topically close to actual facts. I would expect all three architectures to drop in accuracy under adversarial noise, and I have not measured that, so I won’t claim the lead holds.关于这个基准测试没有测试的内容:这里的干扰轮次是通用的闲聊——“我这边没有阻碍”、“听起来不错”。真实的生产噪声在主题上接近实际事实。我预计三种架构在对抗性噪声下准确率都会下降,而我没有测量过这一点,所以我不会声称领先优势依然成立。
On what is missing for production use: real entity extraction (the ingest() interface already accepts a structured triple, so swapping in an LLM-based extractor is a contained change), incremental vector indexing, graph pruning for long-running conversations that accumulate entities indefinitely, and persistent storage. The repo includes a NetworkX-to-Neo4j export path for anyone who needs durability and concurrent multi-agent writes—but that is an optional step, not a performance upgrade. The reasons to make that jump are transactional guarantees and concurrency, not raw query speed.关于生产使用还缺少什么:真正的实体提取(ingest()接口已经接受结构化的三元组,因此替换为基于LLM的提取器是一个可控的变更)、增量向量索引、针对无限累积实体的长期运行对话的图剪枝,以及持久化存储。仓库中包含了一条从NetworkX到Neo4j的导出路径,供任何需要持久性和并发多智能体写入的人使用——但这是一个可选步骤,而非性能升级。进行这种切换的原因是事务保证和并发性,而不是原始查询速度。
What the Numbers Actually Say数字实际上说明了什么
None of this needed a bigger model or a longer context window. Every single result came from changing how information is represented, not how much data gets crammed into a prompt.这一切都不需要更大的模型或更长的上下文窗口。每一个结果都来自于改变信息的表示方式,而非将多少数据塞进提示词。
If you take only one number from this article, take the join-query gap: 80% versus 20–40%. That is the real argument for structured memory, not the token savings.如果你只从这篇文章带走一个数字,那就带走关联查询的差距:80% 对 20-40%。这才是支持结构化记忆的真正论据,而不是token节省。
While the token savings are real and measurable, they are secondary. In this benchmark, questions requiring two facts from completely different parts of the conversation were where the graph architecture showed its largest advantage. That gap held consistently across all five scenarios, not just the ones that happened to be easy for a graph.虽然token节省是真实且可测量的,但它们是次要的。在这个基准测试中,需要来自对话完全不同部分的两个事实的问题,才是图架构展现出最大优势的地方。这个差距在所有五个场景中都保持一致,而不仅仅是那些恰好对图来说很容易的场景。
The full project—five scenarios, three architectures, the test suite that locks these numbers in as regression tests, and the Neo4j export path—is available at the repository below.完整的项目——五个场景、三种架构、将这些数字锁定为回归测试的测试套件,以及Neo4j导出路径——可在下面的仓库中获取。
Full source code: https://github.com/Emmimal/context-graph-benchmark/完整源代码:https://github.com/Emmimal/context-graph-benchmark/
References参考文献
[1] Liu, N. F., Lin, K., Hewitt, J., Paranjape, A., Bevilacqua, M., Petroni, F., & Liang, P. (2024). Lost in the Middle: How Language Models Use Long Contexts. Transactions of the Association for Computational Linguistics, 12, 157–173. https://doi.org/10.1162/tacl_a_00638[1] Liu, N. F., Lin, K., Hewitt, J., Paranjape, A., Bevilacqua, M., Petroni, F., & Liang, P. (2024). 迷失在中间:语言模型如何使用长上下文. 计算语言学协会汇刊, 12, 157–173. https://doi.org/10.1162/tacl_a_00638
[2] Zhang, W., Zhou, Y., Qu, H., & Li, H. (2026). Loosely-Structured Software: Engineering Context, Structure, and Evolution Entropy in Runtime-Rewired Multi-Agent Systems (arXiv:2603.15690). arXiv. https://arxiv.org/abs/2603.15690[2] Zhang, W., Zhou, Y., Qu, H., & Li, H. (2026). 松散结构化软件:运行时重连多智能体系统中的工程上下文、结构与演化熵 (arXiv:2603.15690). arXiv. https://arxiv.org/abs/2603.15690
[3] A. Kollegger, “Context Graphs & Agentic Decisions,” Neo4j Developer Blog, Jan. 31, 2026. [Online]. Available: https://medium.com/neo4j/context-graphs-agentic-decisions-9a125f22f411[3] A. Kollegger, “上下文图与智能体决策,” Neo4j开发者博客, 2026年1月31日. [在线]. 可获取于: https://medium.com/neo4j/context-graphs-agentic-decisions-9a125f22f411
[4] W. Lyon, “When Your Agents Share a Brain: Building Multi-Agent Memory with Neo4j,” Neo4j Developer Blog, Apr. 13, 2026. [Online]. Available: https://medium.com/neo4j/when-your-agents-share-a-brain-building-multi-agent-memory-with-neo4j-bac609f17b23[4] W. Lyon, “当你的智能体共享一个大脑:使用Neo4j构建多智能体记忆,” Neo4j开发者博客, 2026年4月13日. [在线]. 可获取于: https://medium.com/neo4j/when-your-agents-share-a-brain-building-multi-agent-memory-with-neo4j-bac609f17b23
[5] Macklin, N., Zaim, Z., & Erdl, A. (2026). Context Graphs and AI Memory Across the Globe. Neo4j Developer Blog. https://medium.com/neo4j/context-graphs-and-ai-memory-across-the-globe-bb17e293df32[5] Macklin, N., Zaim, Z., & Erdl, A. (2026). 跨全球的上下文图与AI记忆. Neo4j开发者博客. https://medium.com/neo4j/context-graphs-and-ai-memory-across-the-globe-bb17e293df32
[6] NetworkX documentation. https://networkx.org/[6] NetworkX文档. https://networkx.org/
[7] Scikit-learn Developers, “TfidfVectorizer,” Scikit-learn Documentation. [Online]. Available: https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html[7] Scikit-learn开发者, “TfidfVectorizer,” Scikit-learn文档. [在线]. 可获取于: https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html
[8] OpenAI. Counting tokens with tiktoken. https://github.com/openai/tiktoken[8] OpenAI. 使用tiktoken计算token. https://github.com/openai/tiktoken
[9] Neo4j Python Driver documentation. https://neo4j.com/docs/api/python-driver/current/[9] Neo4j Python驱动文档. https://neo4j.com/docs/api/python-driver/current/
Disclosure披露声明
All code in this article was written by me and is original work, developed and tested on Python 3.12 (Windows, PyCharm). Benchmark numbers are from actual runs of the code in the linked repository and are reproducible by cloning it and running benchmark.py and measure_scaling.py, except where the article explicitly notes a number is a heuristic or estimate rather than a measured result. I have no financial relationship with any tool, library, or company mentioned in this article.本文中的所有代码均由本人编写,属于原创作品,在Python 3.12(Windows,PyCharm)上开发和测试。基准数字来自于链接仓库中代码的实际运行结果,通过克隆仓库并运行benchmark.py和measure_scaling.py可以复现,除非文章明确注明某个数字是启发式或估算值而非测量结果。我与本文中提到的任何工具、库或公司均无财务关系。







