---
title: 工作原理
description: 签名算法、为何匹配必须精确，以及账本实际记录的内容。
sidebar:
  order: 2
---
## 签名

每个决策在用作缓存键之前都会被规范化为稳定的签名：

```ts
export function signature(decision: Decision): string {
  const normalized = {
    intent: decision.intent,
    params: sortKeysDeep(decision.params),
  };
  return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
}
```

规范化刻意保持浅层——对象键会被递归排序，因此 `{ a: 1, b: 2 }` 和 `{ b: 2, a: 1 }` 会生成相同的哈希值，但不会触及 `intent` 或参数*值*的任何内容。不会进行词干提取、大小写折叠或语义规范化。

```txt
{ intent: "summarize_pr", params: { repo: "acme/widgets", pr: 482 } }
{ intent: "summarize_pr", params: { pr: 482, repo: "acme/widgets" } }
        │                                       │
        └──────────────── 相同签名 ─────────────┘

{ intent: "summarize_pr", params: { repo: "acme/widgets", pr: 483 } }
        │
        └── 不同签名（不同的 pr）────────────────┘
```

## 为什么使用精确匹配，而非语义匹配

语义/模糊缓存会捕获更多重复请求——“总结这个 PR”和“给我这份拉取请求的摘要”都会命中。但这也意味着 Runic 必须判断两种不同措辞的请求是否足够接近、可以共享缓存答案；而这正是 Runic 被设计为避免做出的那类非确定性判断。

精确匹配意味着缓存命中是可证明为同一个决策，而不只是可能是同一个决策。代价是调用方必须谨慎决定 `params` 中包含什么——参见[快速入门](/quickstart#choosing-intent-and-params)。

## 账本记录的内容

账本是只追加的日志，而非可能产生偏差的累计总额：

```ts
interface LedgerEvent {
  signature: string;
  kind: "hit" | "miss";
  tokensSpent: number;
  timestamp: number;
}
```

- **未命中时**，Runic 会记录你的代码为生成该产物所报告的 `tokensSpent`。
- **命中时**，Runic 会查找该签名最初的未命中记录，并记录与当时保存的*相同* `tokensSpent` 值——绝非估算，也绝非猜测。如果某个正在命中的签名不知为何不存在先前的未命中记录，则会记录 `0`，而不会虚构一个数字。

```txt
summary.totalSpent = sum of all miss events
summary.totalSaved = sum of all hit events
tokensWithoutRunic = totalSpent + totalSaved   (what it would've cost with no cache at all)
savingsPercent     = totalSaved / tokensWithoutRunic
```

Runic 从不自行衡量 token 成本。它只会复述调用代码通过 `storeResult(decision, artifact, tokensSpent)` 告知它的值——参见[基准测试](/benchmarks)，了解 `openrouter-savings` 如何从真实 API 响应而非估算中获取该数值。

## 存储后端

`@runic-labs/cache` 和 `@runic-labs/ledger` 都基于一个小型存储接口构建，v1 附带两种实现：

| 后端 | 生命周期 | 使用场景 |
| --- | --- | --- |
| `MemoryCacheStore` / `MemoryLedgerStore` | 仅限进程生命周期 | 测试、临时脚本、基准测试 |
| `FileCacheStore` / `FileLedgerStore` | 持久化到磁盘（默认 `.runic/`） | CLI 进程与代理进程在没有运行中服务器的情况下读取同一状态 |

两者在 v1 中均为每会话作用域——参见[作用域](/scope)，了解为什么跨会话共享被刻意安排为后续阶段，而不是现在仓促添加。

## 过期状态仅作建议

缓存条目带有 `stale` 标志，该标志根据可配置的时间窗口计算（默认 24 小时）。Runic 永远不会自动驱逐或拒绝过期条目——它只是供调用方在需要时采取行动的信息，而非强制机制。如果你的使用场景需要严格过期，请自行检查 `entry.stale` 并决定如何处理。
