OCAS:自描述的类型化内容寻址存储
作者:星月 | 2026-06-16
一句话介绍
OCAS(Object Content Addressable Store) 是一个自描述的内容寻址存储——每个节点都带一个用 JSON Schema 定义的类型,数据一旦写入就不可变,用内容哈希寻址。它是 AI Agent 和确定性程序之间的类型化数据层:schema 即合约,让 Agent 的结构化产出可以被下游程序直接消费,无需解析。
为什么需要 OCAS?
普通的 KV 存储或文件系统有两个问题:
数据没有类型 —— 存进去的是一坨 JSON 或文本,读出来要靠调用方自己猜结构、自己解析。Agent 产出的数据尤其如此:一段 markdown、一个 JSON,下游程序拿到后还要写一堆 parse 逻辑,格式一变就崩。
数据会变 —— 同一个 key 的内容可以被覆盖,没法回答「这个结果是基于哪个版本的输入算出来的」。对需要审计、追溯、复现的 AI 工作流来说,这是致命的。
OCAS 的解法是把两件事钉死:
- 类型钉死在数据里 —— 每个节点的
type字段就是描述它的 JSON Schema 的哈希。数据自带「我是什么形状」的说明,下游程序按 schema 校验即可,不用猜。 - 内容钉死在哈希里 —— 节点 ID 是它内容的 XXH64 哈希。内容相同则哈希相同,内容一变哈希就变。数据天然不可变、可追溯、可去重。
这就是 ocas 的定位:AI 和确定性程序之间的类型化数据层。在 United Workforce 里,moderator 直接消费 agent 的 typed 产出而无需 parse——因为产出已经是带 schema 的 CAS 节点了。
核心概念
节点:类型化的内容寻址单元
每个 CAS 节点有三部分:
- payload —— 实际数据(一个 JSON 对象)
- type —— 一个哈希,指向描述这个 payload 形状的 JSON Schema
- hash —— 节点自己的 ID,由 payload + type 经确定性 CBOR 编码后取 XXH64 得到,13 字符 Crockford Base32
关键的自指设计:schema 本身也是节点。一个 schema 节点的 type 指向「元 schema(meta-schema)」——一个描述「合法的 JSON Schema 长什么样」的 schema。这条自描述链最终收敛到一个自引用的 bootstrap 节点。
引用:节点构成 DAG
payload 里的字段可以用 format: "cas_ref" 标记成对另一个节点的引用。这样节点之间就连成一张有向无环图(DAG),可以遍历、可以做闭包计算、可以整体导出。
ocas refs <hash> # 列出直接引用边
ocas walk <hash> # 递归遍历整个 DAG
ocas walk <hash> --format tree # 树状视图变量:指向不可变数据的可变指针
节点不可变,但现实需要「可变的名字」——就像 git 的 branch 指向 commit。OCAS 用**变量(variable)**解决:变量是一个名字到哈希的可变绑定。
ocas var set @myapp/config <hash> # 把名字绑到哈希
ocas var get @myapp/config # 查当前绑定
ocas var history @myapp/config # 看历史值(LRU)变量名必须遵循 @scope/name 格式(@ocas/* 为内置保留)。@ 前缀让名字在视觉上和哈希区分开。任何接受哈希的命令也接受变量名——CLI 内部用统一的 resolveHash 解析。
Envelope:自描述的输出格式
每个命令输出一个 { type, value } 信封——type 是结果 schema 的哈希,value 是 payload。输出本身也是自描述、可组合的:
# 一个命令的输出可以直接 pipe 进另一个命令
ocas put @ocas/schema schema.json | ocas render -p
# 用 jq 提取
ocas list --type @ocas/schema | jq -r '.value[].hash'render 是唯一输出原始文本而非信封的命令。
快速上手
# 安装(首次使用自动创建并 bootstrap,无需 init)
pnpm add -g @ocas/cli
# 要求 Node.js >= 22.5.0(用了内置的 node:sqlite)
# 注册一个 schema(schema 也只是被元 schema 约束的节点)
echo '{
"type": "object",
"properties": { "title": { "type": "string" }, "done": { "type": "boolean" } },
"required": ["title", "done"],
"additionalProperties": false
}' | ocas put @ocas/schema -p
# → { "type": "...", "value": "1ABC2DEF34567" }
# 给它起个名字
ocas var set @todo/schema 1ABC2DEF34567
# 存一条 todo
echo '{ "title": "Buy milk", "done": false }' | ocas put @todo/schema -p
# 取出来
ocas get 9XYZ8WVU76543
# 校验完整性 + schema 合规
ocas verify 9XYZ8WVU76543渲染系统:把数据变成文档
OCAS 不只是存数据,还能把 CAS 节点渲染成文本或 HTML 文档。这套渲染基于 LiquidJS 模板 + 三命名空间 + Map-Reduce-Compose 管线。
三个并列的模板命名空间:
| 命名空间 | 作用 |
|---|---|
Instance(@ocas/template/{format}/{type}) | 每种类型一个模板,渲染出内容片段 |
Static(@ocas/template-static/{format}/{type}) | 类型级别的 CSS/JS 资源 |
Compose(@ocas/template-compose/{format}) | 文档外壳,包裹所有内容 + 去重后的静态资源 |
渲染时走三步:Map(DFS 渲染每个节点)→ Reduce(按类型去重收集静态资源,10 个 person 节点只产生 1 份 CSS)→ Compose(套进文档外壳)。
实例模板里可以直接用 payload 字段(自动展开),还有自定义的 render 标签做递归引用展开,带「分辨率衰减(resolution decay)」——越深的引用渲染得越粗略,实现渐进式加载。
ocas template set <type> <file> [--format html] # 设置实例模板
ocas render <hash> [--format html] # 用模板渲染架构
Monorepo,三个包:
packages/
├── core/ # @ocas/core — 核心引擎:哈希、schema、store、verify、bootstrap、render
├── fs/ # @ocas/fs — 文件系统后端(FS 存 CAS + SQLite 存 var/tag)
└── cli/ # @ocas/cli — ocas 命令行工具几个关键设计:
@ocas/core零 SQLite 依赖 —— 核心层不绑定任何存储后端,openStore()返回统一的Store(含cas/var/tag三个子存储)并自动 bootstrap。- 没有「别名」概念 —— 所有名字解析都走
store.var。内置 schema(@ocas/schema、@ocas/string等)在 bootstrap 时作为变量注册,和用户自定义变量一视同仁。 - 统一存储组合 —— FS 后端用文件系统存不可变的 CAS 内容,用 SQLite 存可变的变量和标签,各取所长。
设计哲学
自描述(Self-describing) —— 数据携带自己的类型说明,不依赖外部约定。schema 也是节点,类型系统自我闭环。
不可变 + 内容寻址 —— 相同内容必然同哈希,天然去重、天然可追溯。可以精确回答「这个结果是基于哪些输入算出来的」。
Schema 即合约 —— 这是 ocas 在 AI 工作流里的核心价值。Agent 的产出不是「一段需要解析的文本」,而是「一个带类型合约的节点」。确定性程序可以直接按 schema 消费,类型不匹配在写入时就被拒绝。
CLI 即 API —— 每个命令输出自描述信封,命令之间可以自由组合。整个系统用普通命令行工具就能调试和检查,没有黑盒。
相关链接
- 源码:git.shazhou.work/shazhou/ocas
- npm:@ocas/cli(当前 v0.5.0)
- 上层应用:United Workforce 用 ocas 做不可变的 thread/step 数据层
- 相关方案:Render Verbose Level 方案 — 用单一参数替代多模板的渲染详细度设计