版本:latest

oGMemory 介绍

当 CLI Agent 被持续使用时,上下文很快会从“当前这一轮对话”变成一个更复杂的问题:用户背景、偏好、历史事件、工具经验和项目状态都会不断积累,但模型窗口本身并不会真正记住这些信息。

oGMemory 要解决的正是这层上下文管理问题。它不是把对话简单存成日志,而是把有价值的事实、偏好、事件和经验整理成结构化记忆,并在写入、索引、检索、压缩和归档之间形成完整生命周期,让 Agent 在后续对话中能够重新使用这些长期上下文。

问题:Token 预算是瓶颈

每个 CLI Agent 运行在固定的 token 预算上。上下文窗口既是最昂贵的,也是最稀缺的资源。

症状根因缺失的生命周期阶段
Agent 会遗忘对话早期的内容窗口占满后旧内容被淘汰,没有持久化afterTurn — 缺少抽取 + 持久化
跨会话重复犯同样的错误没有跨会话传递经验的机制session_end 归档 + ② bootstrap 冷启动
一次本该花 $0.05 的查询花了 $0.50扁平检索加载整篇文档,摘要就够了assemble — 缺少分层检索
多 Agent 协作失效Agent 之间看不到彼此的工作上下文afterTurn 跨会话共享 + 多租户隔离
长会话中上下文不断膨胀没有系统化压缩compact — 缺少信号评分 + 摘要链

这不是模型的问题。这是基础设施问题。ContextEngine 提供的正是缺失的那层基础设施。


设计哲学

核心洞察:上下文有生命周期

当前 RAG 系统把检索当作单一操作 — 向量化查询、搜索向量库、返回结果。这忽略了一个基本事实:Agent 系统中的上下文有完整的生命周期,就像数据库中的数据一样。

  诞生           结构化            存储              索引             召回            压缩             归档
  (从对话中      (按类型分类,      (原子写入,        (向量化 +        (向量搜索,      (摘要化,         (会话结束,
   抽取)          按策略路由)      有顺序保证)       写入 L0/L1/L2    分层展开,       去重,            归档,
                                                  IndexRecords)    按预算加载)     压缩)            状态快照)

每个阶段有不同的约束。抽取必须增量。存储必须原子。索引必须异步。检索必须感知预算。压缩必须保留信号。只处理其中一两个阶段的系统把其余阶段留给了偶然。ContextEngine 覆盖完整生命周期。

六个拦截点

Agent 循环不是黑盒。它有明确的执行阶段。关键设计选择:在循环边界拦截,而不是在模型推理内部拦截。 所有上下文操作都发生在推理前后,绝不在推理过程中 — 零延迟影响。

  ┌──────────────────────────────────────────────────────────────────────┐
  │                        Agent Loop(无限循环)                         │
  │                                                                      │
  │   ┌─────┐      ┌──────────┐     ┌──────────┐      ┌─────────┐        │
  │   │ ①   │      │    ②     │     │    ③     │      │   ④     │        │
  │   │ 消息 │────▶│ 推理准备 │────▶│ 工具调用  │────▶│ 轮次结束 │        │
  │   │ 到达 │     │          │     │          │      │         │        │
  │   └──┬──┘      └──────────┘     └──────────┘      └────┬────┘        │
  │      ▲                                                 │             │
  │      │         ┌──────────┐          ┌─────────┐       │             │
  │      │         │    ⑤     │          │   ⑥     │       │             │
  │      └─────────│ 压缩管理  │◀────────│ 会话关闭 │◀─────┘             │
  │                └──────────┘          └─────────┘                     │
  │                                                                      │
  └──────────────────────────────────────────────────────────────────────┘
阶段时机做什么
① 消息到达Agent 推理前解析意图,预取候选上下文
② 推理准备组装上下文窗口冷启动注入 / 主题跟踪 / 预算规划 / 分层加载 / 去重
③ 工具调用工具执行前后注入工具技能 / 参数推导 / 结果压缩 / 事实抽取
④ 轮次结束Agent 完成一轮后增量抽取 / 关系构建 / 冲突解决 / 异步索引
⑤ 压缩管理上下文窗口接近满时信号打分 / 淘汰保护 / 冗余合并 / 摘要链
⑥ 会话关闭会话结束任务归档 / 状态快照 / 完整性审计

上下文类型不平等

七类上下文,各有根本不同的生命周期行为。这不是随意分类 — 由信息本身的语义决定。单一的"用同一种方式存储一切"方案,要么丢失可变状态(如果仅追加),要么破坏不可变历史(如果覆盖写入)。

类型为什么是这个生命周期?写入策略URI 模式
Profile用户状态会变化 — "我住在北京"可能变成"我搬到了东京"Merge — 冲突时新覆盖旧.../memories/profile
Preference偏好按主题累积,每个主题只有一个当前视图按 slug 归并.../memories/preferences/\{slug}
Entity实体累积事实,但"项目 Alpha"仍然是"项目 Alpha"按 slug 归并.../memories/entities/\{slug}
Event历史不可变 — "3月15日完成迁移"永远不会改变仅追加.../memories/events/\{event_id}
Case问题解决轨迹是历史记录仅追加.../memories/cases/\{case_id}
Pattern模式从反复观察中涌现并随时间演化按 slug 归并.../memories/patterns/\{slug}
Skill工具专长累积增长 — 经验越多知识越好累积追加.../skills/\{skill_name}

关键架构决策

为什么用 YAML 驱动 Schema?

问题:在 Python 中硬编码抽取 Schema,每新增一个上下文类型都要改代码。每次改动都要动抽取管线、策略路由、URI 解析三处。

方案:YAML Schema 声明式定义上下文类型。SchemaRegistry 在启动时加载所有 YAML 文件,PolicyRouter 和 URIResolver 根据注册信息自动适配。

不用这个方案会怎样:每次新增类型(比如"Decision"或"Handoff")都是一次跨三个模块的代码变更,引入回归风险。YAML 驱动让新类型变成"加一个配置文件"而不是"改三处代码"。

为什么用 Outbox 模式做异步索引?

问题:向量化 + 写入向量索引需要 100-500ms。如果同步执行,每次 afterTurn 都会阻塞,增加 Agent 响应延迟。

方案:写入完成后投递一个 OutboxEvent,后台 Worker 消费事件、执行 embed + upsert。提供至少一次投递保证,超过最大重试次数移入死信队列。Worker 支持 FOR UPDATE SKIP LOCKED 多进程并发消费。

不用这个方案会怎样:同步索引意味着 Agent 每轮等待额外 100-500ms。在交互式场景中这是不可接受的。或者干脆不做索引,那就没有后续的语义检索能力。

为什么 L0/L1/L2 三级检索?

问题:内容越长,向量相似度反而可能越低 — 而非越高。一个 5000 token 的文档 embedding 必须表示其中每一个概念,导致任何单一主题的信号被稀释。查询"Alice 是做什么的?"时,一篇塞满 Go、Kubernetes、PostgreSQL、迁移策略细节的 L2 内容产生的向量是弥散的,"后端工程师"只是几十个信号中的一个。而一句聚焦的 L0 摘要("Alice 是后端工程师")产生的向量集中精准,直接匹配查询。

方案:每个上下文节点按三种粒度索引。L0 摘要(~100 token)产生聚焦向量 — 精准的主题路标。L1 概述(~500 token)信号居中。L2 完整内容(~5000 token)信息最全但向量弥散。检索引擎对三个层级做一次统一的向量搜索,然后把 L0/L1 命中作为目录入口点,递归展开树结构:当 L0 摘要匹配时,搜索器展开其子节点,发现扁平搜索会遗漏的 L2 内容。分数传播(final = α·child + (1-α)·parent)让强匹配父节点下的边缘 L2 结果获得加成。

  ┌─────────────┐      ┌─────────────┐    ┌──────────────┐
  │  L0 摘要     │     │  L1 概述     │    │  L2 完整内容  │
  │  ~100 token  │    │  ~500 token  │    │  ~5000 token  │
  │  聚焦向量    │     │  平衡信号    │    │   信息全面     │
  │  → 路标定位  │     │  → 决策参考  │    │   但向量弥散   │
  └──────┬───────┘    └──────┬───────┘    └───────┬───────┘
         │                   │                    │
         ▼                   ▼                    ▼
    .abstract.md        .overview.md          content.md

召回优势:仅对 L2 做扁平向量搜索,会遗漏那些因细节稀释导致向量分数低于阈值的相关内容。L0/L1 路标引导搜索器定位到正确的目录,然后树展开发现其下的完整内容 — 包括原始向量分数不高但父主题强匹配的 chunk。

为什么用文件系统抽象?

问题:上下文操作需要原子性、并发控制、权限隔离、垃圾回收。每一样都是独立的工程挑战。

方案:复用文件系统数十年的成熟方案。open/read/write/link/unlink/gc 直接映射到上下文操作。多租户隔离在文件系统层面通过路径中的 account_id + owner_space 强制执行 — 即使调用方有 bug 也无法绕过。

不用这个方案会怎样:要么为每项能力自研解决方案(原子写入、并发锁、权限检查),要么放弃这些能力。文件系统隐喻让我们站在巨人的肩膀上。

为什么用乐观锁做并发写入?

问题:Profile 节点会被用户的所有会话同时写入。需要并发控制。

方案:乐观锁 — 读取当前 .meta.json 版本,仅在版本未变时写入。无竞争时零开销;竞争时优雅失败并重试。无需额外基础设施。

不用这个方案会怎样:分布式锁需要协调服务(etcd/ZooKeeper),增加运维复杂度。不加锁则可能出现"后写覆盖"导致数据丢失。乐观锁在常见场景(无竞争)下性能最优,在罕见场景下安全失败。

为什么用 ReAct 循环做抽取?

问题:如果系统已经知道"Alice 是后端工程师",再次抽取就是浪费。但不知道已有什么就无法判断。

方案:LLM 被赋予一组工具 — read(uri)list(uri)get_relations(uri),以及 extract_* 抽取动作 — 然后在循环中执行。每一轮:先读取已有记忆节点(Reason),判断哪些信息是新的,再调用对应的 extract_* 工具(Act)。如果不确定,就读更多节点继续循环。这是真正的 ReAct:推理和工具调用交替进行,不是单次盲目抽取。

  迭代 1:  read(profile_uri) → "Alice, 后端工程师, 伦敦"
            → 无新信息,跳过

  迭代 2:  read(entities/go) → "Go 专家,偏好错误处理模式 X"
            → 新增: "Alice 现在也在用 Rust 做副项目"
            → extract_entity(slug="rust", ...)

不用这个方案会怎样:盲目的单次抽取要么重复存储已知信息(浪费 token 和存储),要么遗漏隐含的新信息(丢失信号)。ReAct 循环用少量额外成本换取显著更高的抽取质量。