附录B:SHA256 内容指纹与增量检测¶
为什么需要这讲¶
本项目的文档入库系统(第16讲)使用 SHA256 哈希实现增量入库——只更新变化的文件,跳过未变化的。这一机制是 IndexManifest 的核心,但主讲义没有展开解释哈希算法本身。
一、什么是哈希(Hash)¶
哈希函数是一种将任意长度的数据映射为固定长度摘要的算法。
核心特性: 1. 确定性:同一输入永远产生同一输出 2. 雪崩效应:输入改一个比特,输出天差地别 3. 单向性:无法从哈希值反推原始内容 4. 碰撞抵抗:找到两个不同内容产生相同哈希值在计算上不可行
二、为什么选择 SHA256¶
| 算法 | 输出长度 | 碰撞安全性 | 速度 |
|---|---|---|---|
| MD5 | 128 bit | ❌ 已破解 | 快 |
| SHA1 | 160 bit | ❌ 已破解 | 较快 |
| SHA256 | 256 bit | ✅ 安全 | 中等 |
| SHA512 | 512 bit | ✅ 非常安全 | 慢 |
SHA256 是安全性和速度的最佳平衡点。对于文件指纹来说,SHA256 的碰撞概率约为 1/2^128(生日攻击),远低于硬件故障概率。
三、本项目的指纹计算¶
为什么分块读取(8192 字节/块):
- 如果知识库有一个 500MB 的 PDF,f.read() 会把整个文件加载到内存
- 分块读取每次只在内存中保留 8KB,无论文件多大都不会 OOM
四、增量检测机制¶
flowchart TD
File["📄 文件<br/>data/hr_data/入职流程.pdf"] --> Read["分块读取<br/>SHA256 哈希"]
Read --> Hash["指纹:a1b2c3d4e5f6789..."]
Hash --> Check{"IndexManifest<br/>中有此文件记录?"}
Check -->|"❌ 新文件"| Ingest["必须入库"]
Check -->|"✅ 有记录"| Compare{"指纹是否相同?"}
Compare -->|"✅ 相同"| Skip["⏭️ 跳过<br/>文件未变化"]
Compare -->|"❌ 不同"| Delete["🗑️ 删除旧 chunk_ids"]
Delete --> Ingest
Ingest --> Write["💾 写入新 chunk"]
Write --> Update["📝 更新 Manifest<br/>保存新指纹 + chunk_ids"]
style Skip fill:#ECFDF5,stroke:#059669,stroke-width:2px
style Ingest fill:#FFFBEB,stroke:#D97706,stroke-width:2px
具体例子:
五、为什么不用文件修改时间¶
很多开发者第一时间想到用 os.path.getmtime() 来判断文件是否变化。但这不可靠:
| 方法 | 问题 |
|---|---|
mtime |
Git clone 后 mtime 是克隆时间,不是编辑时间;CI 环境 mtime 不稳定 |
| 文件大小 | 修改一个字不改变文件大小,但内容已不同 |
| SHA256 | ✅ 内容变化即指纹变化,跨平台可靠 |
六、在版本号中的应用¶
SHA256 不仅用于文件指纹,还用于生成知识库版本号的配置哈希:
如果版本号的配置哈希不同,说明 Embedding 模型、Reranker 或 Chunk 方案有变化——这是需要重点关注的版本变更。
小结¶
- SHA256 = 任意输入 → 固定 256bit 输出,雪崩效应保证微小差异可检测
- 分块读取避免大文件撑爆内存
- 增量检测 = 文件指纹比较,跳过未变化文件
- 优于 mtime:Git clone、CI 环境下 mtime 不可靠