先把场景放在桌面上。你让 coding agent 修报销系统的重复扣款。它第一次调查时读过 billing 仓库、支付接口文档和三段测试日志, 最后发现问题可能在幂等键;你又明确说过“这里不要 mock 数据库”。两天后开一个新会话,只说: “继续上次那个报销 bug,先验证是不是租户配置导致的。”

这句话需要四种上下文一起到场:仓库和文档是资源,上次做到哪里的摘要是会话历史, “不要 mock 数据库”是长期记忆,而检查租户配置可能还要一套技能或工作流。 传统做法通常给它们各找一个家:文件进知识库,偏好进 memory store,技能留在本地目录,会话再由 agent runtime 保存。 每一层都能工作,真正麻烦的是下一轮回答要重新猜:去哪里找、找到以后读多深、哪些内容真的该进 prompt。

OpenViking 的核心答案不是“换一个更强的向量库”,而是先给上下文建立统一地址、层级和生命周期。 搜索只返回候选;原始内容仍由内容存储作证;集成层再决定什么时候检索、什么时候读取 L2、怎样组装本轮 model view。 这条边界如果读顺了,后面的 L0/L1/L2、find/search、session commit 就不再是一堆新名词, 而是一条连续的上下文供应链。

阅读契约。 读完这一篇,应该能沿着同一个报销 bug 回答五件事:OpenViking 为什么把 resource、memory、skill 放进同一棵树; 一份仓库怎样先落原文、再异步生成 L0/L1 与索引;L0/L1/L2 为什么是阅读深度而不是三段记忆; findsearch 怎样找到候选;一次 session commit 为什么要拆成可恢复的两个阶段。

证据边界。 本文以 OpenViking 官方文档与公开仓库为事实基础,源码固定在 cfd74413。 文档说明公开概念与 API;源码说明 ingest、retrieval 与 session commit 的实际调用链;“适合什么系统”属于基于这些证据的工程判断。 文档中标为规划中的 agent endpoint、tool/payment 等能力不写成已实现事实,项目自报 benchmark 也不作为本文结论。

一、它不是给 Agent 加一个记忆盒子,而是重画上下文的地址簿

1.1 三类材料为什么要进同一棵树

回到报销 bug。仓库里的 billing/retry.py 不会因为 agent 读过一次就变成用户记忆; “不要 mock 数据库”也不应该混进公共项目文档;一套查租户配置的步骤更像可复用技能。 OpenViking 没有抹掉这些差别,而是让它们共享一套可寻址约定:先用 URI 表达“谁拥有它、它属于哪类上下文”, 再让不同类型保留各自生命周期。

viking://
├── resources/                       # 公开或账户级资源
├── user/{user_id}/
│   ├── memories/                    # 从交互与任务中沉淀的持久知识
│   ├── resources/                   # 用户私有资源
│   ├── skills/                      # 用户私有技能
│   └── sessions/{session_id}/       # 当前消息与历史归档
└── agent/
    └── skills/                      # 账户内共享技能

这棵树的价值首先是 ownership。公共支付文档可以放在 viking://resources/; 用户专属的项目约束进入 viking://user/{user_id}/memories/; 某个团队共享的检查流程可以进入 viking://agent/skills/。 检索时系统不只拿到一段相似文本,还能拿到 URI、上下文类型、层级和作用域。 官方的上下文类型文档 也明确把 resource 解释为用户添加的静态材料,把 memory 解释为交互中学到的持久知识,把 skill 解释为可执行能力。

报销场景里的材料 更自然的归属 为什么不能混在一起
billing 仓库、支付接口文档 resource 它们是外部证据,更新来自源材料,不来自一次聊天里的模型判断。
“这个项目不要 mock 数据库” memory 它来自用户交互,需要按用户和策略隔离,并可能在未来会话召回。
检查租户配置的可靠步骤 skill 它表达“怎么做”,不只是关于项目的一条事实。
上次调查到幂等键的过程 session archive 它先是完整证据,归档后才可能按策略生成摘要与长期记忆。

1.2 “filesystem” 说的是交互方式,不是“数据库就是一堆本地文件”

OpenViking 常把自己称为 context filesystem。最容易产生的误读,是把这句话理解成“所有东西直接存在磁盘目录里”。 实际上,viking:// 是上层 URI 抽象:它提供类似 lsreadmvabstractoverviewfind 的访问方式,底层仍然把内容存储索引存储分开。 filesystem 解决的是地址、层级和操作习惯,不能拿来推断部署一定简单,或数据只能落在本机文件系统。

二、一份仓库怎样长成上下文树:原文先落稳,语义随后补齐

现在把 billing 仓库导入。最危险的做法,是一边让模型总结,一边把总结当成唯一存档:模型调用失败,资源就半进半出; 摘要写偏了,原文也失去独立证据。OpenViking 的 ingest 路径先解析源材料并构建最终 URI,再提交内容, 后面的 L0/L1 与向量索引由语义队列继续处理。这里的顺序比具体类名更重要:可恢复的原文先成为事实,模型生成的表示后到。

OpenViking 资源摄取链路,从 repo 和 docs 经过 parse tree、RAGFS commit、semantic queue,异步生成 L0、L1 与 vector index
摄取不是一次同步的“切块加 embedding”:解析与内容提交先完成,语义摘要和索引通过队列异步补齐,因此新资源可能经历短暂的最终一致窗口。

2.1 源码里的五步,比 README 的一句 add 更有解释力

ResourceProcessor.process_resource() 的 docstring 直接列出 parse、URI metadata、source commit、vector index、summarize;实现先让 media processor 把内容写进临时区, 再由 TreeBuilder.finalize_from_temp() 生成最终 URI 与 temp_uri 元数据,随后 ResourceProcessor 调用 VikingFS.persist_temp_tree() 才把临时树提交到正式位置。解析器负责理解文件形状,TreeBuilder 负责定地址,VikingFS 负责落盘;它们都不是“让 LLM 读一遍仓库然后凭空造树”。

# 只保留源码调用链的骨架
parse_result = await media_processor.process(source=path, ...)
context_tree = await tree_builder.finalize_from_temp(
    temp_dir_path=parse_result.temp_dir_path,
    scope=scope,
    to_uri=to,
)
root_uri = context_tree.root.uri
temp_uri = context_tree.root.temp_uri
await viking_fs.persist_temp_tree(temp_uri, root_uri, ...)
# 原文提交完成后,语义队列再生成 L0/L1 与索引

语义处理器的职责也写得很直白: 自底向上生成 .abstract.md.overview.md, 再把这些表示向量化。子目录摘要先形成,父目录才有材料写自己的 overview;因此仓库不是被切成一袋互不相干的 chunk, 而是在保留目录关系的前提下长出一套导航层。

工程边界。 add_resource 成功与“每个节点的语义表示已经可检索”不是同一个时刻。异步队列、重试和 circuit breaker 给了摄取路径恢复能力,也带来了最终一致性。若业务要求“刚上传就必须搜到”,调用方要等待处理状态,而不能把 add 返回当作索引屏障。

三、L0、L1、L2:不是短期、中期、长期,而是同一份上下文的阅读深度

仓库进树以后,agent 不应该为一句“租户配置在哪里读”就吞下整个 billing 目录。OpenViking 给同一节点准备三种表示: L0 是很短的 abstract,用来快速判断这条路值不值得走;L1 是带结构的 overview,用来理解这一支有哪些材料、该下钻到哪里; L2 才是原始文件或完整内容。它们回答的是“读多深”,不是“记多久”。

OpenViking L0 L1 L2 阅读深度图,L0 用于选方向,L1 用于理解分支,L2 用于核对原始内容,并划掉把三层误解为记忆时间线的读法
同一份支付文档可以同时有 L0、L1、L2:先扫摘要,再看目录概览,只有确认相关时才读原文。层级减少盲读,不取代原文。
viking://resources/billing/
├── .abstract.md       # L0:这是什么,约百 token
├── .overview.md       # L1:这一支有哪些内容、怎样继续读
├── .relations.json    # 关联 URI
├── tenant-config.md   # L2:完整文档
└── retry.py           # L2:原始代码

官方上下文层级文档 把 L0 的用途写成向量搜索与快速过滤,把 L1 写成 rerank 与内容导航,把 L2 写成按需加载的完整内容。 这个区分也解释了为什么检索结果里带 abstract 与 level 很有用:调用方可以先决定路径,再支付读取原文的 token 成本。

3.1 文件树负责作证,向量索引负责带路

有了三层表示,还要避免另一个误读:向量库里既然有 abstract,是不是它就成了真正的数据源? OpenViking 的存储文档 给出的边界相反:RAGFS 一侧保存 L0/L1/L2 与关系,是内容 source of truth;向量库保存 URI、向量和元数据,用来找到候选。 删除、移动或重命名需要同步维护两边,但回答“原文到底写了什么”时,证据仍从内容层读取。

OpenViking 存储职责边界图,RAGFS 保存权威内容,vector index 返回 viking URI 候选,集成层读取 L2 并组装 model view
三层不要揉成一个“memory database”:索引负责找到 URI,RAGFS 负责提供原文,model view 是调用方最后组装出的本轮可见材料。

四、find 不做会话规划,search 可以先规划任务:随后走的是同一个检索器

新会话里那句“继续上次那个报销 bug”,字面上没有 billing、幂等键、租户配置这些完整关键词。 如果应用已经知道要找什么,find 会把原始 query 直接交给检索器;如果应用把当前 session 一起交给 search,系统还可以先读归档摘要、最近消息和当前 query,把模糊请求拆成几条有类型、有优先级的查询。 规划之后,两条 API 都调用 HierarchicalRetriever.retrieve():分界在要不要先做 session-aware query planning, 不在“一个只查向量、另一个只查树”。

OpenViking find 与 search 检索图,find 提交原始 query,search 可结合归档摘要、最近五条消息与当前 query 生成 typed queries,两条入口汇合到同一个 HierarchicalRetriever,再由 quick 或 thinking 模式检索 MatchedContext 候选
find 不做会话意图规划;search 在有 session context 且 intent 开启时先做 query planning。 quick / thinking 都属于共享检索器:thinking 沿高分目录继续下钻,quick 更接近平铺向量召回。

4.1 intent analyzer 不是“理解一切”,而是把本轮问题改写成受约束的查询计划

VikingFS.search() 先取得最新 archive overview 和当前消息;在 intent 开启且上下文存在时,构造 IntentAnalyzer(max_recent_messages=5)。 analyzer 把压缩摘要、最近五条消息、当前问题和可选目标目录摘要交给 query planner,再生成多个 TypedQuery; 没有 session context,或 intent 被关闭时,就回退为原始 query。这意味着 search 的“更懂任务”有明确条件,不是无条件多做一次 LLM 调用。

# 同一句模糊请求,可能被规划成受约束的三条查询
TypedQuery("billing duplicate charge idempotency", context_type=RESOURCE, priority=1)
TypedQuery("do not mock database", context_type=MEMORY, priority=2)
TypedQuery("inspect tenant configuration", context_type=SKILL, priority=2)

上面是便于理解的示意,不是项目保证的固定输出。真正的合约在 IntentAnalyzer: 每条 query 带 context_typeintentpriority;模型解析失败会暴露为错误,而不是悄悄假装计划成功。

4.2 thinking 模式为什么不是“多搜几次”

层级检索的关键,是目录本身也有 L0/L1。quick 模式在目标范围里直接做向量搜索并排序;thinking 模式先对 L0/L1 做全局搜索, 把高分目录放进优先队列,再只对这些目录的 children 继续搜索。 递归循环 会记录 visited URI、按分数传播、可选 rerank,并且只对非 L2 节点继续下钻——L2 文件是终点,不再当目录展开。

最后得到的是 MatchedContext:URI、level、abstract、score 等候选信息,并按 memory/resource/skill 分类。 这一步非常容易被产品介绍写成“相关上下文自动进入模型”,但源码边界不是这样:find/search 负责找到,read/overview 负责读取, 应用或插件负责决定本轮给模型看什么。如果只接了 MCP endpoint,却没有让 agent 主动调用工具,也没有 hooks 自动召回,数据库不会自己跳进 prompt。

五、会话怎样变成记忆:commit 先交接证据,再异步提炼

agent 修到一半要结束会话。最简单的 commit 是同步调用模型生成摘要,再清空旧消息;但只要模型超时、进程重启或写入中断, 就可能同时丢掉原消息和摘要。OpenViking 当前源码把 session commit 拆成两个阶段:Phase 1 在路径锁内重新读取权威消息, 规划保留窗口、写 raw archive、入持久队列并发布 ready marker;Phase 2 再由队列消费者生成 archive summary 与策略允许的 memory。

OpenViking session commit 两阶段图,Phase 1 持久化原始 messages jsonl、ready marker 与 task id 后返回 accepted;Phase 2 异步生成 summary,并仅在 policy 允许且实际生成差异时写可选的 memory diff,最后写 done 标记
commit_async() 在 Phase 1 的证据与交接都持久化后就能返回 accepted;耗时的总结与受 policy 控制的可选记忆提取留给可恢复的 Phase 2。

5.1 Phase 1 的目标不是“快”,而是建立不可含糊的交接点

Session.commit_async() 先在 path lock 内重载 messages.jsonl 与 meta,因为别的 worker 手里的 Session 对象可能已经过期;然后按消息数或 turn/token budget 规划 archive 与 retained tail。它先写 Phase 1 intent,再把归档消息写入 history/archive_NNN/messages.jsonl, 入 SESSION_COMMIT 持久队列、注册 task、改写 live root,最后把 phase1.status 标成 ready。

{
  "status": "accepted",
  "task_id": "...",
  "archive_uri": "viking://user/.../sessions/.../history/archive_003",
  "archived": true
}

这个返回值表达的是“后台任务已经被可靠接住”,不是“长期记忆已经生成”。调用方若要在紧接着的下一步读取新记忆,仍应跟踪 task 状态。 但即使进程在 accepted 之后重启,原始 archive、队列消息和 ready marker 已经提供了恢复起点。

5.2 Phase 2 会提炼什么,由 policy 决定;.done 永远最后写

队列消费者最终调用 resume_queued_commit()。 它先检查 .done.failed.json 与 Phase 1 ready 状态,再读 raw archive;真正进入 Phase 2 后,还会等待前一个 archive 完成, 避免并发归档改变记忆顺序。

archive summary、long-term memory、execution memory 并不是每次都全开。 源码先按 memory policy、agent evolution 开关和允许的类型判断有没有工作, 再并发执行满足条件的任务;成功的长时记忆变更可落成 memory_diff.json。所有阶段完成、relations 与 active_count 更新后, .done 最后写。 进度 marker 用于恢复时避免把已经成功的 memory step 盲目重放。

不要把“self-evolving”理解成无条件自我改写。 profile、preferences、entities、events、trajectories、experiences、tools、skills 等类型在项目里都有位置,但实际提取受配置、memory policy、 用户/peer scope 与 agent-evolution 开关约束。更准确的说法是:OpenViking 提供一条可配置、可追踪的记忆提炼管线,而不是每轮都自动学会一切。

六、数据库有了上下文,谁把它送进这一轮回答

到这里,OpenViking 已经能存、找、读、归档,但报销 agent 仍可能一无所知。原因很朴素:数据库能力和 runtime 时机是两回事。 MCP 可以暴露 findsearchrecallremember 等工具,让 agent 主动调用; hooks/plugin 则可以在 SessionStart、用户输入前、每轮结束和 compaction 前自动执行固定动作。两条路最后都要经过应用的 model-view assembler。

OpenViking 集成边界图,底层 service 提供 find、search、read 与 session commit;中层 MCP 暴露 find、search、recall、read、list、remember,Codex hooks 在 SessionStart、UserPromptSubmit、Stop、PreCompact 触发;顶层组装 model view,强调 search 不等于 inject
服务层提供能力,MCP 或 hooks 决定何时调用,应用决定哪些候选要继续读到 L2、以什么预算进入本轮 model view。

官方的 Codex 集成文档 给出一条具体生命周期:SessionStart 加载 profile 与索引,UserPromptSubmit 前做相关记忆召回,Stop 时追加新对话,PreCompact 前补齐并 commit。 它也明确记录一个失败边界:SIGTERMCtrl+C/exit 不会触发 hook,孤儿 session 要等下次启动回收。 这类细节比“接入后自动拥有记忆”更重要,因为它告诉我们哪些时刻可能漏捕获、怎样恢复。

七、它应该放进 Agent Memory 系列,但补的是 memory layer 与平台之间的空位

OpenViking 不需要另开一个系列。它仍然在回答 Agent Memory 的核心问题:谁保存历史,谁提炼长期知识,谁检索,谁组装 model view。 但它把系列视野向外推了一层:不只管理从对话抽出的 memory record,还让 repo、docs、session archive 与 skill 共享地址和分层读取协议。 因此最自然的位置,是放在 TencentDB Agent Memory 之后、Cognee / Supermemory 之前。

项目 最先解决的压力 OpenViking 与它的分界
Mem0 从对话抽取、检索与排序长期 memory records。 OpenViking 把 memory 放进更大的 resource / skill / session 上下文树。
TencentDB Agent Memory 当前工具日志如何 offload,并沿 L0-L3 沉淀长期用户理解。 OpenViking 的 L0/L1/L2 是统一上下文的阅读深度,且强调 URI、摄取、检索与 session 持久队列。
OpenViking 散落在资源、记忆、技能和会话里的上下文怎样统一寻址、分层读取、可恢复更新。 它更像 context substrate,不负责替应用做完整 agent orchestration。
Cognee / Supermemory 把知识图谱、连接器、文件处理、profile/search API 产品化。 它们更靠平台/API 边界;OpenViking 更突出 filesystem 交互、层级检索与白盒 session lifecycle。

7.1 它也不是完整 agent framework

OpenViking 有 MCP、插件和 VikingBot 等集成,但核心仓库最稳定的责任仍是 context service:摄取材料、组织 URI、生成分层表示、检索候选、 保存 session 并按策略提炼 memory。它自己会调用模型生成摘要、规划查询和抽取记忆;但任务规划、面向用户的答案生成、代码执行、审批与动作编排仍要由上层 runtime 负责。 把它理解成“上下文底座”比“另一个全能 agent”更接近源码边界。

八、什么时候值得用:先看你是否真的需要一条上下文供应链

OpenViking 把很多难题放进一套系统,同时也把运行面做厚了:Python service、Rust RAGFS、向量数据库、异步 QueueFS、embedding/VLM、 可选 reranker 与 query planner 都可能进入部署。语义摘要、intent query 与 memory extraction 还会引入模型误差; 当前快照的最新修复之一就与减少 summary 中不受支持的实体幻觉有关。再加上主仓库 AGPL-3.0,生产接入前需要同时评估运维、质量与许可证边界。

适合:资源、会话、长期记忆和技能必须一起被检索。

例如 coding agent、研究 agent、企业知识 agent:任务会跨会话,原文证据必须可追溯,单一 top-k 向量检索已经不够。

适合:调用方愿意显式管理 model view。

团队接受“先找候选、再读深层、最后按预算注入”的集成责任,而不是期待数据库自动把正确 prompt 拼好。

不一定适合:只有少量静态文档和简单问答。

如果一个普通 RAG pipeline 已经能稳定回答,完整的 session、queue、memory policy 与层级语义处理可能大于收益。

不一定适合:写入后要求强同步可见,且不能接受模型生成表示。

语义队列意味着最终一致,L0/L1、intent 与 memory 都可能受模型质量影响;这类系统需要额外状态等待、评测与失败回退。

用一句话收束:OpenViking 不是让 agent “多记一点”,而是让上下文从散落材料变成一棵有地址、有阅读深度、有恢复点的树。 对报销 agent 来说,真正的收益不是新会话里凭空“想起”幂等键,而是它能解释自己从哪条 session archive 找到线索, 沿哪支 resource tree 读到租户配置,又为什么把“不 mock 数据库”作为用户约束放进本轮 model view。 当这条证据链清楚以后,memory 才不只是一个神秘的相似度盒子。

参考资料