为什么用本地不可变 CAS 保存证据底座

向量库和图数据库都是可重建投影;CAS 保存 Raw、IR 与 CPG,Manifest Generation 负责引用这些不可变对象。

本页目录 · 9 节

向量库很适合回答「哪些片段最像这个问题」,却不适合回答「这段片段究竟由哪份原始字节、哪个 Parser 和哪个 Commit 生成」。一旦只留下 Embedding 和一段截断文本,Parser 升级、Chunk 变化或答案出错时,系统无法完整重放当时的证据链。

因此 mt_rag 把本地 Filesystem CAS 作为事实底座:Raw Snapshot、DocumentIR、完整 CPG 和其他不可变 Artifact 按内容 Hash 存储;Qdrant、FTS 和 Neo4j 只是面向查询的可重建投影。

本篇知识地图

flowchart LR
  B["Exact Bytes"] --> H["SHA-256"]
  H --> U["cas://sha256/digest"]
  U --> O["Immutable Object"]
  O --> R["CAS Reference<br/>size / media type / encoding"]
  R --> I["DocumentIR / Full CPG"]
  I --> M["Immutable Manifest Generation<br/>references CAS digests"]
  M --> P["Vector / Keyword / Graph Projection"]
  P -. "rebuild" .-> M

这里需要区分四个概念:Content Address、Hash Domain、Atomic Publication 和 Lineage Reference。

CAS 保存的是精确字节,不是「看起来相同的文本」

当前 LocalFilesystemCas 使用 URI:

cas://sha256/<64 hex digest>

对象实际落在:

data/00-cas/01-objects/sha256/<前2位>/<第3-4位>/<完整digest>

两份完全相同的字节得到同一个地址,因此天然去重;任意一位改变都会产生新对象。CasObjectRefV1 同时记录 Digest、Byte Size、Media Type 和 Encoding,让消费者不必从文件名猜内容类型。

CAS Digest 只表示存入对象的精确字节。它不能和 original_content_hash 混为一谈。文本内容身份可能执行 Unicode NFC、CRLF → LF 等规范化,而 CAS 仍然对原始字节 Hash;同一文本的 CRLF 与 LF 文件可以拥有相同的 Canonical Content Hash,却有不同的 CAS Digest。这两个结果都正确,因为它们回答不同问题。

Hash 不是一个字段,而是一组不可互换的域

system_contracts.pyHashRefV1 明确 Hash Type 与 Canonicalization:

Hash Domain 输入 变化时影响
blob_sha256 CAS 中精确字节 产生新的不可变对象
original_content_hash 规范化后的原始内容 表示 Source Content 变化
record_payload_hash 生产者选择的 Versioned Metadata 触发 Payload Update,而非必然重做 Embedding
cleaned_content_hash 清洗输出 Parser/Cleaner 变化可独立追踪
summary_hash Summary 文本 不污染 Original Content Identity
chunk_content_hash Chunk 文本 决定 Chunk/Embedding 是否重算

Freshness、Observed At、Rerank Score 和 LLM Summary 都不允许进入 original_content_hash。否则页面即使一字未改,也会因为每天重新观察而制造新内容、重做 Embedding,并破坏幂等。

一次写入怎样避免半对象

当前实现不是直接打开目标路径覆盖写入,而是:

  1. 计算完整 Byte Digest,推导目标路径;
  2. 如果对象已存在,重新验证 Digest 与 Size,返回同一 Reference;
  3. 在同一目录创建带 PID、Thread ID 和 UUID 的唯一临时文件;
  4. 写入、Flush、fsync,设置 File Mode;
  5. 使用 os.replace 原子发布;
  6. 发布后再次读取并校验 SHA-256 与 Size;
  7. 无论成功失败都清理临时文件。
sequenceDiagram
  participant P as Producer
  participant T as Temp file
  participant C as CAS object
  P->>P: sha256(exact bytes)
  P->>C: exists?
  alt already exists
    C-->>P: verify digest + size
  else new object
    P->>T: exclusive create + write + fsync
    T->>C: atomic replace
    C-->>P: verify digest + size
  end

这提供的是单机文件系统边界内的安全发布,不是跨机器分布式事务。Manifest 仍然负责说明某次 Run 引用了哪些对象、使用什么 Parser Version、生成了哪些投影。

CAS 也要防路径攻击和静默损坏

内容寻址不自动等于安全。当前实现还做了几层约束:

  • CAS Root 和路径组件都不能是 Symlink;
  • 派生路径必须位于 Root 内,URI 只能包含一个合法 SHA-256 Digest;
  • 默认目录权限为 0700,对象权限为 0600
  • verify_on_read=true 时每次读取重新计算 Digest,并核对 Expected Size;
  • 对象不是 Regular File、缺失或 Digest 不匹配都会抛 CasIntegrityError,不会返回残缺字节。

这些检查不会把 CAS 变成 Secret Manager。企业原始内容仍然需要磁盘加密、主机访问控制、备份与保留策略;CAS 解决的是内容身份、完整性和可重放性。

为什么完整 CPG 留在 CAS,Neo4j 只放 Semantic Projection

完整 Code Property Graph 包含 AST、CFG、DFG、Call Edge 等大量节点。把所有 Syntax Node 塞进 Neo4j,会显著增加存储和遍历成本,也让开发查询被低层语法噪声淹没。

当前 store_full_cpg 把完整 Graph JSON 写入 CAS;semantic_cpg_projection 只选择 Repository、File、Class、Method、Function、API 等检索相关节点,以及 CALLSIMPORTSREADSWRITESROUTES_TO 等关系进入查询图。这样需要审计时能回到 Full CPG,日常查询又不必承受完整语法图成本。

失败模式与取舍

失败模式 当前保护 Trade-off
写入中进程退出 唯一 Temp + Atomic Replace,目标对象不会半写 可能留下 Temp,Finally/维护任务需清理
已存在对象被损坏 Put/Read 时重新 Hash,立即报 Integrity Error Verify-on-read 增加磁盘读取成本
Symlink 指出 Root 逐层 lstat 并拒绝 Symlink 实现比普通 Path Write 更复杂
Parser 升级 Raw Object 不变,新 IR/Projection 产生新 Hash 磁盘占用增长,换取可比较与可回滚
CAS 对象无限增加 当前 Append-only,不把自动 GC 当默认动作 删除需要先分析 Manifest Reachability 与保留策略
只备份 Query Store 无法还原原始证据 备份重点应是 CAS、Manifest 和配置,而不是只备份向量点

可复核的项目证据

  • src/mt_rag/core/cas.py:URI 解析、分片路径、权限、Symlink 防护、Atomic Write 和 Verify-on-read;
  • src/mt_rag/core/content_identity.py:Bytes/Text/JSON 的 Canonicalization 与 Hash;
  • src/mt_rag/core/system_contracts.pyCasObjectRefV1HashRefV1 及各 Hash Domain;
  • src/mt_rag/code/cpg.py:Full CPG 入 CAS 与 Semantic CPG 投影边界;
  • config/rag-runtime.jsoncas 段:Root、算法、权限和 Verify 配置;
  • docs/02-runbooks/local-filesystem-cas.md:本地操作与完整性验证说明。

参考资料

CAS 的价值不是「多存一份文件」,而是把可重建查询投影和不可替代原始证据分开:索引可以重做,证据身份不能事后编造。

相关文章

在做类似的事情?

很乐意就分布式系统、交付流程和应用 AI 交换意见。

[email protected]