本文作者:AYi(@AYi_AInotes)。版权归作者所有,未经授权禁止转载。
做 RAG 应用,最危险的不是检索不到,而是新旧事实一起被检索出来:客户三月确认预算 50 万,六月改成 30 万,Agent 却回答 "预算在 30 万到 50 万之间"。
我起初以为加时间戳、按最新排序就能解决,后来才发现:
时间戳只能判断先后,不能表达 "新事实已经取代旧事实"。普通向量库把两条记录平行召回,塞进 Prompt,最后还是让模型猜哪条是真的。
这不是偶发 bug,是向量检索的结构性缺陷。这篇跟大家保姆级拆解时序上下文的四个核心机制,以 Graphiti 和 OpenContext 为参照,讲清怎么落地、怎么选,以及它解决不了什么。

先看事故现场
构造一个最小案例,假设你在给一家律所做 Agent,知识库里有这样几条记录:
2024-03-15 客户 A 项目预算确认为 50 万
2024-04-20 项目负责人由张律师变更为李律师
2024-06-01 客户 A 项目预算调整为 30 万(原 50 万作废)
2024-06-10 合同 V3 签署,替代 V2
2024-07-05 李律师离职,项目移交王律师现在问 Agent 三个问题:
- 客户 A 的项目预算是多少?
- 这个项目现在谁负责?
- 当前有效的合同版本是哪个?
如果你用标准 RAG——文本切分、向量化、top-k 召回、拼进 Prompt——大概率会出现这些情况:
- 问预算,同时召回"50 万“和”30 万"两条,模型要么取其一,要么含糊其辞;
- 问负责人,可能召回张律师、李律师、王律师三条,模型不知道哪条是当前状态;
- 问合同版本,V2 和 V3 的语义相似度几乎一样高,检索无法区分。
其实并不是模型不够聪明,关键在于数据层没有告诉它哪条是有效在线的。
一、为什么向量检索治不了这个病

你可能会想:给每条记录加个时间戳,检索时按时间排序取最新的那条,不就行了?
但实际没那么简单,咱们把问题拆成三层看。
第一层:向量空间没有时间轴。
时间戳能告诉你"哪条更新",但不能告诉你"旧的那条是否已经作废"。一条三月的记录可能仍然有效(比如客户名称没变),另一条三月的记录可能早就被六月的新决定推翻了。"更新"不等于"取代",而向量检索对这两件事一视同仁。
更基础的问题是,"预算 50 万“和”预算 30 万"在 embedding 空间里的距离非常近——它们讲的是同一件事,只是数字不同。余弦相似度不会因为一条是三月写的、一条是六月写的就给出不同分数。检索系统看到的是两条同等相关的结果。
第二层:检索不理解"取代"关系。
在真实业务里,六月那条"预算调整为 30 万"不是一条独立信息,它的语义是"废止前一条,以这条为准"。但向量数据库里没有"废止"这个概念。两条记录是平行存在的,没有谁取代谁。
第三层:把矛盾甩给模型是错误的分工。
当两条互相矛盾的信息同时出现在 Prompt 里,你实际上是在要求 LLM 做一个它没有足够信息完成的判断——哪条是当前有效的?模型可能猜对,也可能猜错,而且你无法预测它什么时候会猜错。
这是一个数据层的结构性问题,不应该在推理层用概率去赌。
二、时序上下文的核心思路

解决方案的方向其实不复杂:给每条信息标记它的有效期,并且在信息变更时建立明确的"取代"关系。
这个思路在数据库领域并不新鲜——bi-temporal modeling(双时态建模)在金融和保险行业用了几十年,但在 Agent Memory 领域,它直到最近才被认真对待。
OpenContext 是 Alloomi 团队最近开源的一个项目,它的自我定位很直接:the context runtime substrate that powers agentic applications。不是 UI,也不是 chat surface,更不是模型提供商——是 Agent 应用底下那层“胶水”:持久记忆、检索、上下文纠错、多平台连接、定时感知,以及把这些粘在一起的 embedding 持久化层。
它的核心数据结构叫 Temporal Context Graph——一个有向无环图,每个节点(fact)都有 valid_from / valid_until 字段,所有修正都是 append-only,不做破坏性覆盖。
用它的术语来讲,上面那个预算案例会变成:
Fact: "客户A项目预算=50万"
valid_from: 2024-03-15
valid_until: 2024-06-01 (被下一条 supersede)
Fact: "客户A项目预算=30万"
valid_from: 2024-06-01
valid_until: null (当前有效)查询时,系统不是做相似度匹配,而是先确定查询时间点(默认是"现在"),然后只返回在该时间点有效的事实。过期事实不会进入 Prompt,矛盾在数据层就被解决了。
三、四个关键机制

不管具体用哪个框架,时序上下文要解决的核心问题可以拆成四个机制。OpenContext 把它们浓缩成了四个动词 API——remember、recall、improve、forget——刚好一一对应。
1. 有效期(valid_from / valid_until)
每条信息不是永真的。它有一个生效时间和一个失效时间。失效时间为空表示"当前仍然有效"。
这看起来简单,但它改变了检索的基本逻辑:从"找最相关的"变成"找最相关且当前有效的"。一个 where 条件的差别,结果完全不同。
在 OpenContext 里,recall 可以传入一个 as-of 时间戳,过滤条件是 valid_from ≤ t < valid_until——你可以问“上周二用户相信什么”,也可以问“现在哪条偏好是当前的”。
2. 取代关系(supersede)
新信息不覆盖旧信息,而是"接替"它。旧记录保留在系统里,但被标记为已失效,同时记录是被哪条新信息取代的。
回到前面那个预算案例——系统不只是知道"30 万比 50 万新",而是明确记录了"30 万这条事实废止了 50 万那条事实"。这意味着你可以回答两类问题:
- "现在的预算是多少?"——返回当前有效的那条;
- "三月份的时候预算是多少?"——返回当时有效的那条。
历史不丢失,但不会污染当前查询。
在 OpenContext 里,这对应 improve 动词——它在图上追加一条 supersession 或 contradiction 边,原始节点永远不会被硬删除。
3. 软删除与遗忘
有些信息需要从系统中退役——比如客户要求删除个人数据,或者某条记录被证明是错误的。
硬删除意味着这条信息从未存在过,审计时无法追踪。软删除(或者叫"遗忘")的做法是:标记这条信息已退役,记录退役时间和原因,查询时默认不返回,但审计时仍然可以看到它曾经存在过。
这对企业合规很重要。数据删除请求(比如 GDPR 的"被遗忘权")需要你能证明"已经删了",而不是"从来没有过"。
OpenContext 的 forget 动词做的就是这件事:valid_until = now,GDPR 的 right-to-erasure 由一个独立的合规流程处理,不走主 API 路径。
4. 审计日志与证据链
每个"当前状态"都应该能回溯到它是怎么来的:
- 原始信息是什么;
- 什么时候被谁修改;
- 修改的依据是什么;
- 当前版本是第几次变更的结果。
这不只是合规需求。当 Agent 给出一个基于历史信息的回答时,用户可能会追问"你这个结论的依据是什么?"——如果系统能返回完整的证据链,比如"这条信息来自 6 月 1 日的预算调整通知,它取代了 3 月 15 日的原始预算确认",可信度完全不同。没有证据链的回答,本质上还是让用户信任一个黑盒。
OpenContext 的做法是结构化审计日志写入 ~/.opencontext/logs/audit.jsonl,加上 Fernet 对称加密保护敏感字段、URL 白名单/黑名单控制外发调用。
四、两条开源路线

前面讲的四个机制是通用思路。落到实操,目前有两条值得关注的开源路线。
路线一:OpenContext——可嵌入的时序上下文运行时
OpenContext(github.com/melandlabs/opencontext)是 Alloomi 团队开源的 context runtime,Apache 2.0 协议。它不是一个 UI 产品,也不是一个向量数据库——它是一层可以嵌入任何 host process 的运行时基底。
核心能力:
Temporal Context Graph. 有向无环图,每个 fact 节点带 valid_from / valid_until. Supersession、contradiction、merge 是一等公民边,修正是 append-only.
四动词 API。 remember(写入,基于 scope + content-hash 幂等)、recall(统一检索,跨语义、词法、图和时间维度)、improve(追加取代/矛盾/合并边)、forget(软删除)。这四个动词覆盖了一条事实的完整生命周期。
Platform Integration Mesh. 统一的 IntegrationRecord 格式,覆盖 Gmail、Slack、Telegram、Linear、Jira、iMessage、飞书、微信等——凭证轮换、限流、重连逻辑都在 adapter 后面。
Deterministic Loop Engine. 一个调度器,先判断有没有真正需要处理的事,有才调用 Agent Runtime。LLM 调用不是基础——是最后一步。
Library-First 安装。 pnpm add @melandlabs/opencontext 一行搞定,不依赖 React、Next 或 Tauri。同时提供 MCP Server 模式,可以直接接入 Claude Desktop、Cursor、Claude Code、Codex CLI 等 MCP-capable 的 Agent Runtime.
它和纯向量数据库的区别在于:Pinecone、Weaviate、Qdrant 做的是相似度匹配,OpenContext 在这之上加了时序图——事实会被取代,不只是被相似度排序。它和纯记忆库的区别在于:它不只是一个 library,而是一个 runtime——HTTP daemon、MCP server、CLI、集成网格和循环引擎都包含在内。
路线二:Graphiti / Zep——专注时序记忆引擎
Graphiti 是 Zep 团队开源的时序知识图谱引擎,底层跑在 Neo4j 或 FalkorDB 上,设计目标是给任何 Agent 提供时序记忆能力。它的论文报告了在 Deep Memory Retrieval benchmark 上相比 MemGPT 最高 18.5% 的准确率提升,同时延迟降低 90%。Zep 是基于 Graphiti 的商业化托管服务。
它的定位很纯粹:一个可插拔的记忆层。你的 Agent 框架不管是 LangChain、LlamaIndex 还是自研的,都可以把 Graphiti 接进来当时序记忆后端。适合有工程能力、想深度定制的团队。
怎么选
两者不是直接竞品,更像是不同切面的解决方案:
- Graphiti 是一个纯粹的时序知识图谱引擎。你已经有 Agent 框架,想加一层时序记忆后端,它是最直接的选择。需要自己部署 Neo4j,自己处理数据接入。
- OpenContext 是一个完整的 context runtime。它不只做时序记忆,还包含集成网格(多平台数据汇入)、循环引擎(调度和唤醒)、检索原语(chunking + embedding + 多后端适配)和 Agent Runtime 接口。如果你在从零搭建一个需要长期记忆的 Agent 应用,它提供的是一个更完整的基底层。
简单说:Graphiti 解决“时序记忆怎么存和查”,OpenContext 解决“一个有记忆的 Agent 应用底下需要什么”。
五、用开头那个案例跑一遍 OpenContext

前面构造的律所案例,用 OpenContext 的四动词 API 走一遍完整流程。以下代码基于 @melandlabs/opencontext 的公开接口,Node 22.6+ 可直接运行。
Step 1:remember——写入原始事实
import { createMemoryStore } from "@melandlabs/opencontext";
const store = createMemoryStore({
backend: "sqlite-vec", // 桌面端默认,也可选 postgres / indexeddb
embeddingProvider: "local",
});
// 三月:预算确认
await store.remember({
userId: "lawyer-agent",
scope: "client-a",
messages: [
{
role: "system",
content: "客户A项目预算确认为50万",
platform: "email",
timestamp: new Date("2024-03-15").getTime(),
},
],
embedOnInsert: true,
});remember 基于 (scope, content-hash) 幂等——同一条事实重复写入不会产生重复节点。
Step 2:recall——按时间点检索当前有效事实
// 三月底查询:返回 "预算50万"
const marchResult = await store.recall({
userId: "lawyer-agent",
query: "客户A的项目预算是多少",
asOf: new Date("2024-03-30").getTime(),
sources: ["memory"],
limit: 5,
});
// marchResult.results → [{ content: "客户A项目预算确认为50万", ... }]recall 的 asOf 参数对应过滤条件 valid_from ≤ t < valid_until。不传则默认“现在”。
Step 3:improve——六月预算变更,建立取代关系
// 六月:预算调整,旧事实被 supersede
await store.improve({
userId: "lawyer-agent",
scope: "client-a",
original: { content: "客户A项目预算确认为50万" },
edge: "supersession", // 取代关系
replacement: {
content: "客户A项目预算调整为30万(原50万作废)",
timestamp: new Date("2024-06-01").getTime(),
},
});improve 在图上追加一条 supersession 边。原始节点的 valid_until 被设为 2024-06-01,新节点的 valid_from 为 2024-06-01、valid_until 为 null(当前有效)。原始节点永远不会被硬删除。
Step 4:再次 recall——矛盾已在数据层解决
// 七月查询:只返回当前有效的 "预算30万"
const julyResult = await store.recall({
userId: "lawyer-agent",
query: "客户A的项目预算是多少",
sources: ["memory"],
limit: 5,
});
// julyResult.results → [{ content: "客户A项目预算调整为30万...", ... }]
// 但如果你想回溯历史:
const historyResult = await store.recall({
userId: "lawyer-agent",
query: "客户A的项目预算是多少",
asOf: new Date("2024-04-01").getTime(),
sources: ["memory"],
limit: 5,
});
// historyResult.results → [{ content: "客户A项目预算确认为50万", ... }]注意:七月查询时,“50 万”那条不会出现在结果里——矛盾在数据层就被过滤掉了,不需要模型去猜。
Step 5:forget——客户要求删除个人数据
await store.forget({
userId: "lawyer-agent",
scope: "client-a",
content: "客户A项目预算调整为30万(原50万作废)",
reason: "客户要求删除个人数据 (GDPR Art.17)",
});
// valid_until = now,查询不再返回,但审计日志保留记录forget 是软删除:valid_until = now。GDPR 的 right-to-erasure 由独立合规流程处理,不走主 API 路径。审计日志(~/.opencontext/logs/audit.jsonl)仍然可以证明“这条数据曾经存在过,已按要求退役”。
六、从 runtime 到产品:Alloomi 做了什么
OpenContext 是基础设施层——它解决的是“一个有记忆的 Agent 应用底下需要什么”。但对终端用户来说,没有人想直接操作一个 runtime。
Alloomi 是基于 OpenContext 构建的面向专业人员的 AI coworker 产品。它把 OpenContext 的能力包装成了一个可以直接使用的桌面端工作助手。
从被动检索到主动感知。 OpenContext 提供了 Deterministic Loop Engine(循环引擎),Alloomi 用它实现了 Attention Agent——不是“你问我答”,而是系统主动判断“这件事需要你决定”。回到律所案例:当客户 A 的预算从 50 万变成 30 万时,系统不只是更新记忆图谱,还会主动提醒负责律师“预算发生了变更,相关合同条款可能需要调整”。
跨平台的上下文汇聚。 OpenContext 的 Platform Integration Mesh 覆盖了 Gmail、Slack、Notion、飞书、微信等几十个平台。Alloomi 用它把散落在不同工具里的业务信息统一到同一条时间线上——“预算变更”来自一封邮件,“负责人变更”来自一条 Slack 消息,“合同版本”来自一个 Notion 页面,系统自动识别它们属于同一个项目、同一个客户。
经验随工作积累。 Alloomi 的核心主张是“让 AI 每完成一次交付,就获得一次成长”。OpenContext 的 remember → improve 循环让每次业务变化都变成可追溯的经验节点。对于律所场景,这意味着:处理过的合同版本、客户偏好变化、审批流程中的判断——这些工作过程数据不再随对话窗口关闭而消失,而是沉淀为 Agent 的长期记忆。
本地优先,数据不出机器。 所有上下文数据存在本地,AES-256 加密,有完整审计日志。对于律所、金融、保险这类对数据主权敏感的行业,这是一个硬性前提。
简单说:OpenContext 是引擎,Alloomi 是基于这个引擎造出来的车。开发者可以直接用 OpenContext 造自己的车;不想从零开始的专业团队,可以直接开 Alloomi。
七、真实边界

机制讲完了,接下来是更重要的问题:它不能做什么?
时序上下文解决的是外部记忆层的正确性,不等于模型"学会了"。
即使系统能准确返回当前有效的信息,模型本身并没有因此变得更有经验。它仍然是在每次查询时临时获取上下文,然后生成回答。真正的"经验内化"——让模型不依赖检索就能做出更好判断——需要后训练闭环,那是另一个更难的问题。
实体统一仍然很难。
"张律师""张三""Zhang San""项目负责人张"可能指的是同一个人。时序上下文能追踪一个实体的状态变化,但前提是系统已经正确识别了"这是同一个实体"。实体消歧在真实企业数据里仍然是个硬问题。
不是所有场景都需要时序上下文。
如果你的知识库是一份产品文档、一个 FAQ 列表或一组技术规范——内容基本不随时间变化——标准 RAG 完全够用。时序上下文的价值出现在信息会频繁更新、会互相取代、需要区分"曾经正确“和”现在正确"的场景。典型的包括:
- 客户关系管理(偏好、预算、决策人会变);
- 法律事务(合同版本、条款修改、判例更新);
- 金融投资(持仓、评级、市场判断会变);
- 项目管理(负责人、进度、优先级会变);
- 人事与组织(职位、汇报关系、权限会变)。
如果你的业务不涉及这类动态信息,不必过度设计。
八、对开发者来说,现在能做什么
如果你正在构建 Agent 应用,想在记忆层加入时序能力,四条路可以走:
1. 用 OpenContext 作为 context runtime. pnpm add @melandlabs/opencontext 一行安装,四个动词 API 覆盖事实的完整生命周期。支持 SQLite-vec(桌面端)、Postgres(服务端)、IndexedDB(浏览器)多后端,同时提供 MCP Server 模式直接接入 Claude Code、Cursor 等 Agent Runtime。适合从零搭建需要长期记忆的 Agent 应用。
2. 用 Graphiti 自建时序记忆层。 开源、Apache 2.0、文档齐全、有论文背书。需要自己部署 Neo4j,自己处理数据接入和查询逻辑。适合已有 Agent 框架、想加一层时序记忆后端的团队。
3. 用 Zep Cloud 托管服务。 如果不想自己运维图数据库,Zep 提供开箱即用的 API。返回的 context block 可以直接塞进 Prompt。适合快速验证。
4. 直接用 Alloomi. 如果你的需求不是“给自己的 Agent 加记忆”,而是“团队需要一个能长期跟踪业务状态的 AI 工作助手”,Alloomi 提供了基于 OpenContext 的完整产品体验——桌面端、主动提醒、跨平台连接、本地数据,开箱即用。
5. 在现有 RAG 上加最小时序层。 如果暂时不想引入新框架,最简单的改进是:给每条知识库记录加一个 valid_until 字段,检索时过滤掉已失效的记录。这不能解决所有问题(比如取代关系和实体统一),但能挡住最常见的"返回过期信息"bug。
九、回到最初的问题
Agent 记忆里的"过期与冲突"看起来是个工程细节。但如果你退一步想,它指向一个更大的判断:
Agent 的长期记忆不应该是一个越堆越大的向量库。它应该是一个能持续追踪人物、项目、判断和时间变化的系统——知道什么是当前状态,什么已经被取代,什么曾经正确但现在不再适用。
这件事做对了,Agent 才有可能从"每次都重新开始"走向"理解业务是怎么演变到今天的"。而理解演变,是积累经验的前提。
相关资源:
- OpenContext 开源仓库:github.com/melandlabs/opencontext
- OpenContext 架构文档:github.com/melandlabs/opencontext/blob/main/docs/architecture.md
- Alloomi 官网:alloomi.ai
- Graphiti 开源仓库:github.com/getzep/graphiti
- Zep 论文:arxiv.org/abs/2501.13956
待核实事项:
- Graphiti 论文中报告的 18.5% 准确率提升和 90% 延迟降低数字来自其 DMR benchmark 评测,具体测试条件建议查阅原论文。