Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Cache expensive file processing results using SHA-256 content hashes — path-independent, auto-invalidating, with service layer separation.
.claude/skills/affaan-m-content-hash-cache-pattern/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 37% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 47% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 8% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 10% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 55% | 0% |
使用 SHA-256 内容哈希作为缓存键,缓存昂贵的文件处理结果(PDF 解析、文本提取、图像分析)。与基于路径的缓存不同,此方法在文件移动/重命名后仍然有效,并在内容更改时自动失效。
--cache/--no-cache CLI 选项时使用文件内容(而非路径)作为缓存键:
pythonimport hashlib from pathlib import Path _HASH_CHUNK_SIZE = 65536 # 64KB chunks for large files def compute_file_hash(path: Path) -> str: """SHA-256 of file contents (chunked for large files).""" if not path.is_file(): raise FileNotFoundError(f"File not found: {path}") sha256 = hashlib.sha256() with open(path, "rb") as f: while True: chunk = f.read(_HASH_CHUNK_SIZE) if not chunk: break sha256.update(chunk) return sha256.hexdigest()
为什么使用内容哈希? 文件重命名/移动 = 缓存命中。内容更改 = 自动失效。无需索引文件。
pythonfrom dataclasses import dataclass @dataclass(frozen=True, slots=True) class CacheEntry: file_hash: str source_path: str document: ExtractedDocument # The cached result
每个缓存条目都存储为 {hash}.json —— 通过哈希实现 O(1) 查找,无需索引文件。
pythonimport json from typing import Any def write_cache(cache_dir: Path, entry: CacheEntry) -> None: cache_dir.mkdir(parents=True, exist_ok=True) cache_file = cache_dir / f"{entry.file_hash}.json" data = serialize_entry(entry) cache_file.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") def read_cache(cache_dir: Path, file_hash: str) -> CacheEntry | None: cache_file = cache_dir / f"{file_hash}.json" if not cache_file.is_file(): return None try: raw = cache_file.read_text(encoding="utf-8") data = json.loads(raw) return deserialize_entry(data) except (json.JSONDecodeError, ValueError, KeyError): return None # Treat corruption as cache miss
保持处理函数的纯净性。将缓存作为一个单独的服务层添加。
pythondef extract_with_cache( file_path: Path, *, cache_enabled: bool = True, cache_dir: Path = Path(".cache"), ) -> ExtractedDocument: """Service layer: cache check -> extraction -> cache write.""" if not cache_enabled: return extract_text(file_path) # Pure function, no cache knowledge file_hash = compute_file_hash(file_path) # Check cache cached = read_cache(cache_dir, file_hash) if cached is not None: logger.info("Cache hit: %s (hash=%s)", file_path.name, file_hash[:12]) return cached.document # Cache miss -> extract -> store logger.info("Cache miss: %s (hash=%s)", file_path.name, file_hash[:12]) doc = extract_text(file_path) entry = CacheEntry(file_hash=file_hash, source_path=str(file_path), document=doc) write_cache(cache_dir, entry) return doc
| 决策 | 理由 | |----------|-----------| | SHA-256 内容哈希 | 与路径无关,内容更改时自动失效 | | {hash}.json 文件命名 | O(1) 查找,无需索引文件 | | 服务层包装器 | 单一职责原则:提取功能保持纯净,缓存是独立的关注点 | | 手动 JSON 序列化 | 完全控制冻结数据类的序列化 | | 损坏时返回 None | 优雅降级,在下次运行时重新处理 | | cache_dir.mkdir(parents=True) | 在首次写入时惰性创建目录 |
python# BAD: Path-based caching (breaks on file move/rename) cache = {"/path/to/file.pdf": result} # BAD: Adding cache logic inside the processing function (SRP violation) def extract_text(path, *, cache_enabled=False, cache_dir=None): if cache_enabled: # Now this function has two responsibilities ... # BAD: Using dataclasses.asdict() with nested frozen dataclasses # (can cause issues with complex nested types) data = dataclasses.asdict(entry) # Use manual serialization instead
--cache/--no-cache 选项的 CLI 工具| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-06 | fail→pass | 12,077 | 7,860 | -35% | 1 | 1 | 0% | 2,311 | 3,160 | +37% | 0 | 0 | — |
case-01 | fail→pass | 16,353 | 16,416 | +0% | 1 | 1 | 0% | 3,551 | 5,233 | +47% | 0 | 0 | — |
case-02 | fail→pass | 20,704 | 14,407 | -30% | 1 | 1 | 0% | 4,557 | 4,926 | +8% | 0 | 0 | — |
case-03 | fail→pass | 24,008 | 18,330 | -24% | 1 | 1 | 0% | 4,945 | 5,427 | +10% | 0 | 0 | — |
case-04 | fail→pass | 13,857 | 10,127 | -27% | 1 | 1 | 0% | 2,214 | 3,421 | +55% | 0 | 0 | — |
case-05 | pass→pass | 13,742 | 6,484 | -53% | 1 | 1 | 0% | 2,361 | 2,711 | +15% | 0 | 0 | — |
case-07 | fail→pass | 11,490 | 7,693 | -33% | 1 | 1 | 0% | 2,155 | 2,998 | +39% | 0 | 0 | — |
case-08 | pass→pass | 11,426 | 8,059 | -29% | 1 | 1 | 0% | 1,990 | 2,954 | +48% | 0 | 0 | — |
case-09 | pass→pass | 13,156 | 7,094 | -46% | 1 | 1 | 0% | 2,296 | 2,839 | +24% | 0 | 0 | — |
case-10 | pass→pass | 13,307 | 9,112 | -32% | 1 | 1 | 0% | 2,313 | 3,201 | +38% | 0 | 0 | — |
case-11 | fail→pass | 15,290 | 15,139 | -1% | 1 | 1 | 0% | 2,722 | 4,048 | +49% | 0 | 0 | — |
case-12 | fail→pass | 13,235 | 7,645 | -42% | 1 | 1 | 0% | 2,207 | 2,923 | +32% | 0 | 0 | — |
case-13 | fail→pass | 9,890 | 4,530 | -54% | 1 | 1 | 0% | 1,841 | 2,358 | +28% | 0 | 0 | — |
case-14 | pass→pass | 10,040 | 4,118 | -59% | 1 | 1 | 0% | 1,544 | 2,216 | +44% | 0 | 0 | — |
case-15 | pass→pass | 7,821 | 7,361 | -6% | 1 | 1 | 0% | 1,546 | 2,882 | +86% | 0 | 0 | — |
case-16 | pass→pass | 16,084 | 12,396 | -23% | 1 | 1 | 0% | 2,638 | 3,602 | +37% | 0 | 0 | — |
case-17 | pass→pass | 13,814 | 12,357 | -11% | 1 | 1 | 0% | 2,104 | 3,498 | +66% | 0 | 0 | — |
case-18 | fail→pass | 11,466 | 12,011 | +5% | 1 | 1 | 0% | 1,863 | 3,276 | +76% | 0 | 0 | — |
case-19 | pass→pass | 9,323 | 4,888 | -48% | 1 | 1 | 0% | 1,359 | 2,289 | +68% | 0 | 0 | — |
case-20 | pass→pass | 15,305 | 9,889 | -35% | 1 | 1 | 0% | 2,404 | 3,052 | +27% | 0 | 0 | — |
case-21 | pass→pass | 10,292 | 7,890 | -23% | 1 | 1 | 0% | 1,980 | 3,015 | +52% | 0 | 0 | — |
case-22 | fail→pass | 17,126 | 13,987 | -18% | 1 | 1 | 0% | 2,644 | 3,866 | +46% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +50 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.