先把场景放在桌面上。你让 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 为什么是阅读深度而不是三段记忆;
find 和 search 怎样找到候选;一次 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 抽象:它提供类似 ls、read、mv、
abstract、overview、find 的访问方式,底层仍然把内容存储和索引存储分开。
filesystem 解决的是地址、层级和操作习惯,不能拿来推断部署一定简单,或数据只能落在本机文件系统。
二、一份仓库怎样长成上下文树:原文先落稳,语义随后补齐
现在把 billing 仓库导入。最危险的做法,是一边让模型总结,一边把总结当成唯一存档:模型调用失败,资源就半进半出; 摘要写偏了,原文也失去独立证据。OpenViking 的 ingest 路径先解析源材料并构建最终 URI,再提交内容, 后面的 L0/L1 与向量索引由语义队列继续处理。这里的顺序比具体类名更重要:可恢复的原文先成为事实,模型生成的表示后到。
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 才是原始文件或完整内容。它们回答的是“读多深”,不是“记多久”。
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、向量和元数据,用来找到候选。 删除、移动或重命名需要同步维护两边,但回答“原文到底写了什么”时,证据仍从内容层读取。
四、find 不做会话规划,search 可以先规划任务:随后走的是同一个检索器
新会话里那句“继续上次那个报销 bug”,字面上没有 billing、幂等键、租户配置这些完整关键词。
如果应用已经知道要找什么,find 会把原始 query 直接交给检索器;如果应用把当前 session 一起交给
search,系统还可以先读归档摘要、最近消息和当前 query,把模糊请求拆成几条有类型、有优先级的查询。
规划之后,两条 API 都调用 HierarchicalRetriever.retrieve():分界在要不要先做 session-aware query planning,
不在“一个只查向量、另一个只查树”。
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_type、intent 与 priority;模型解析失败会暴露为错误,而不是悄悄假装计划成功。
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。
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 可以暴露 find、search、recall、remember 等工具,让 agent 主动调用;
hooks/plugin 则可以在 SessionStart、用户输入前、每轮结束和 compaction 前自动执行固定动作。两条路最后都要经过应用的 model-view assembler。
官方的
Codex 集成文档
给出一条具体生命周期:SessionStart 加载 profile 与索引,UserPromptSubmit 前做相关记忆召回,Stop 时追加新对话,PreCompact 前补齐并 commit。
它也明确记录一个失败边界:SIGTERM、Ctrl+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 向量检索已经不够。
团队接受“先找候选、再读深层、最后按预算注入”的集成责任,而不是期待数据库自动把正确 prompt 拼好。
如果一个普通 RAG pipeline 已经能稳定回答,完整的 session、queue、memory policy 与层级语义处理可能大于收益。
语义队列意味着最终一致,L0/L1、intent 与 memory 都可能受模型质量影响;这类系统需要额外状态等待、评测与失败回退。
用一句话收束:OpenViking 不是让 agent “多记一点”,而是让上下文从散落材料变成一棵有地址、有阅读深度、有恢复点的树。 对报销 agent 来说,真正的收益不是新会话里凭空“想起”幂等键,而是它能解释自己从哪条 session archive 找到线索, 沿哪支 resource tree 读到租户配置,又为什么把“不 mock 数据库”作为用户约束放进本轮 model view。 当这条证据链清楚以后,memory 才不只是一个神秘的相似度盒子。
参考资料
- volcengine/OpenViking
- OpenViking source snapshot: cfd74413
- Architecture
- Context types
- L0 / L1 / L2 context layers
- Storage architecture
- Retrieval
- Session management
- Codex memory plugin lifecycle
ResourceProcessor.process_resource()SemanticProcessorbottom-up contractVikingFS.find()andsearch()HierarchicalRetrieverquick/thinking split- Recursive child search
Session.commit_async()Phase 1- Restart-safe Phase 2