为什么用本地不可变 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.py 用 HashRefV1 明确 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,并破坏幂等。
一次写入怎样避免半对象
当前实现不是直接打开目标路径覆盖写入,而是:
- 计算完整 Byte Digest,推导目标路径;
- 如果对象已存在,重新验证 Digest 与 Size,返回同一 Reference;
- 在同一目录创建带 PID、Thread ID 和 UUID 的唯一临时文件;
- 写入、Flush、
fsync,设置 File Mode; - 使用
os.replace原子发布; - 发布后再次读取并校验 SHA-256 与 Size;
- 无论成功失败都清理临时文件。
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 等检索相关节点,以及 CALLS、IMPORTS、READS、WRITES、ROUTES_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.py:CasObjectRefV1、HashRefV1及各 Hash Domain;src/mt_rag/code/cpg.py:Full CPG 入 CAS 与 Semantic CPG 投影边界;config/rag-runtime.json的cas段:Root、算法、权限和 Verify 配置;docs/02-runbooks/local-filesystem-cas.md:本地操作与完整性验证说明。
参考资料
- Git Internals:Git Objects;
- Git Hash Function Transition 官方文档;
- NIST FIPS 180-4 Secure Hash Standard;
- Python
os.replace官方文档; - W3C PROV-O。
CAS 的价值不是「多存一份文件」,而是把可重建查询投影和不可替代原始证据分开:索引可以重做,证据身份不能事后编造。
相关文章
增量投影怎样让 Vector、Keyword 和 Graph 独立演进
分层 Hash、Delta Planner、Consumer Cursor 和 Shadow Projection 共同解决更新、失败恢复与回滚。
RAG 不是一条必经流水线,而是一种按需取证的能力
先理解目标、先看当前源码,只有跨来源信息真的能改善判断时,才调用 Local RAG 去找证据候选。