Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← 人工智能 / Artificial Intelligence

智能体工程 / Agent Engineering

1. Agent 工程体系全景 / Agent Engineering System Overview

2. Function Calling - 让 LLM 具备行动能力 / Function Calling for Giving LLMs the Ability to Act

3. Agent 框架演进 - 从裸 SDK 到 LangGraph / The Evolution of Agent Frameworks from Raw SDKs to LangGraph

4. RAG 基础 - 让 Agent 拥有"知识" / Retrieval-Augmented Generation Fundamentals for Agent Knowledge

5. 记忆管理 - Agent 的大脑 / Memory Management as the Brain of an Agent

6. Agent 工作流 - 从单步到复杂的执行编排 / Agent Workflows from Single Steps to Complex Orchestration

7. 多 Agent 系统 - 多个 Agent 协作 / Multi-Agent Systems and Agent Collaboration

8. RAG 进阶 - 企业级知识库实战 / Advanced RAG for Enterprise Knowledge Bases

9. 真实 Agent 应用场景 / Real-World AI Agent Applications

10. Structured Output - 让 LLM 输出可控的结构化数据 / Structured Output for Controllable, Machine-Readable LLM Responses

11. Tools Design Best Practices - AI Agent 工具设计最佳实践 / Tools Design Best Practices for AI Agents

12. Agent 架构模式 - 从单 Agent 到多 Agent 的工程范式 / Agent Architecture Patterns

13. Agent Modes — 编程 Agent 的交互模式设计 / Designing Interaction Modes for Coding Agents

14. Agent Workflow 编排:从循环到持久化执行的演进

15. Context Engineering - 从 Prompt 设计到上下文编排 / Context Engineering: From Prompt Design to Context Orchestration

16. Agent 缓存工程:从 KV Cache、Prompt Cache 到语义缓存 / Agent Caching Engineering

17. Harness Engineering, Skills, and Loop Engineering — 从信任模型到验证系统 / From Trusting Models to Verifying Systems

18. MCP 协议 - AI 工具的"USB 接口" / Model Context Protocol for AI Tool Integration

19. Agent 评估与测试 — 如何衡量一个"不可预测"的系统 / Agent Evaluation and Testing — How to Measure an "Unpredictable" System

20. 安全沙箱 - Agent 的安全边界 / Secure Sandboxes as Agent Safety Boundaries

21. 权限与门卫 - Agent 的安全控制中枢 / Permissions and Policy Gates for Agent Control

22. API Key 管理与安全 - Agent 的密钥生命周期的管理 / API Key Lifecycle Management and Security for Agents

23. 提示词注入防护 - Agent 的防御前沿 / Prompt Injection Defense for AI Agents

24. 可观测性与调试 - Agent 运行的透明度保障 / Observability and Debugging for Transparent Agent Operations

25. 模型路由 - 让正确的模型做正确的事 / Model Routing for Matching Models to Tasks

26. OpenClaw 设计深度分析 - 为什么它让人觉得"活"了 / OpenClaw Design Analysis and the Illusion of Liveliness

27. Claude Code 泄露源码深度分析 - 512,000 行代码揭示的生产级 Agent 架构 / Claude Code Source Analysis and Production Agent Architecture

28. LobeChat 设计深度分析 - 全栈 Agent Chat 应用工程实践 / LobeChat Design Analysis and Full-Stack Agent Chat Engineering

29. 编程 Agent 全面对比:从 Claude Code 到 Pi 的设计哲学 / Coding Agents Comparison: Design Philosophies from Claude Code to Pi

30. 领域 Agent 的确定性工具编译与延迟执行——从自然语言规格到单次 CAE 提交

31. Agent 工程学习指南 / An AI Agent Engineering Learning Guide

本页目录

Agent 缓存工程:从 KV Cache、Prompt Cache 到语义缓存 / Agent Caching Engineering ​

📅 创建时间:2026-07-29 🏷️ 标签:#AgentCaching #PromptCaching #KVCache #PrefixCache #SemanticCache #CostOptimization 📚 前置知识:[[14-context-engineering]] [[/04-ai/01-llm-engineering/09-decoder-only-llm]]


📋 本章目标 ​

  • 区分 CPU/数据库缓存、KV Cache、Prefix Cache、响应缓存和语义缓存
  • 理解“缓存命中”究竟复用了哪一段工作,而不是只会背命中率
  • 从 Prefill、Decode 和 Token 前缀解释 Prompt Cache 的底层原理
  • 掌握缓存友好的 Agent 上下文布局、Append-Only 历史和稳定工具定义
  • 能为 Tool、RAG、多 Agent 和 Coding Agent 设计分层缓存
  • 掌握缓存键、TTL、版本、租户隔离、失效、污染和可观测性
  • 能估算缓存对 TTFT、Token 成本和吞吐量的影响

第0部分:先回答最容易混淆的问题——“命中缓存”到底命中了什么? ​

“这个请求命中缓存了。”这句话本身几乎没有信息量。你必须继续问:哪一层缓存?缓存键是什么?复用了什么计算?返回值是否直接复用?

同一个 Agent 请求,可能依次经过六类完全不同的缓存:

text
用户请求
   │
   ▼
┌──────────────────────────────────────────────────────────────┐
│ ① 响应缓存:相同请求是否可以直接返回完整旧答案?             │
└──────────────────────────┬───────────────────────────────────┘
                           │ miss
                           ▼
┌──────────────────────────────────────────────────────────────┐
│ ② 语义缓存:有没有“意思相近”的旧问题和可信答案?             │
└──────────────────────────┬───────────────────────────────────┘
                           │ miss
                           ▼
┌──────────────────────────────────────────────────────────────┐
│ ③ RAG / Tool 缓存:检索、Embedding、API、文件分析能否复用?   │
└──────────────────────────┬───────────────────────────────────┘
                           │ 组装 messages
                           ▼
┌──────────────────────────────────────────────────────────────┐
│ ④ Prompt / Prefix Cache:相同 Token 前缀能否跳过 Prefill?   │
└──────────────────────────┬───────────────────────────────────┘
                           ▼
┌──────────────────────────────────────────────────────────────┐
│ ⑤ KV Cache:本次生成时,历史 Token 的 K/V 是否已经保存?     │
└──────────────────────────┬───────────────────────────────────┘
                           ▼
┌──────────────────────────────────────────────────────────────┐
│ ⑥ 基础设施缓存:模型权重、容器层、数据库页、CDN 等            │
└──────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

其中,第④层命中时,模型仍然会生成一个新答案;第①层命中时,应用通常根本不会调用模型。这是理解 Agent 缓存的第一条分界线。

0.1 一张表先建立全局认识 ​

缓存类型缓存的内容常见缓存键命中后还调用模型吗主要收益
KV Cache历史 Token 的 Key/Value 张量当前序列与请求内部状态会加速逐 Token Decode
Prefix/Prompt Cache已计算前缀的中间状态完全一致的 Token 前缀会降低 Prefill 延迟和输入成本
精确响应缓存完整模型输出模型、参数和规范化请求的哈希不会最大限度降低成本与延迟
语义缓存问题、向量和旧答案Embedding 相似度 + 约束通常不会复用意思相近的问题
Tool Cache工具调用结果工具版本 + 规范化参数视流程而定避免重复 I/O 和外部 API
RAG CacheEmbedding、候选文档、重排结果查询、索引版本、过滤条件会降低检索延迟与费用

0.2 Cache Hit、Cache Miss 和 Cache Invalidation ​

text
请求 key ──查询──> Cache
                    │
          ┌─────────┴─────────┐
          │                   │
       找到了              没找到/已过期
       Cache Hit            Cache Miss
          │                   │
       复用结果          执行真实计算或 I/O
                              │
                              └──写回 Cache

数据已变化,但旧值仍在 Cache
          │
          └──必须删除、过期或换版本号:Cache Invalidation
1
2
3
4
5
6
7
8
9
10
11
12
13
14

缓存最难的通常不是“存进去”,而是判断旧值什么时候不再可信。


第1部分:KV Cache——一次生成内部的模型状态缓存 ​

1.1 为什么自回归生成会重复计算? ​

Decoder-Only 模型一次只生成一个新 Token。假设 Prompt 是 A B C:

text
第1步:输入 A B C       → 生成 D
第2步:输入 A B C D     → 生成 E
第3步:输入 A B C D E   → 生成 F
1
2
3

如果每一步都重新计算所有历史 Token 的 Key 和 Value,A B C 会被反复计算。Causal Attention 决定了过去 Token 的表示不会被未来 Token 改写,因此历史 K/V 可以保存。

text
没有 KV Cache

Step 1: [A K/V][B K/V][C K/V]              → D
Step 2: [A K/V][B K/V][C K/V][D K/V]       → E
Step 3: [A K/V][B K/V][C K/V][D K/V][E K/V]→ F
         └──────反复计算──────┘

有 KV Cache

Step 1: 计算 A B C 的 K/V,保存              → D
Step 2: 读取 A B C,只计算 D 的 K/V           → E
Step 3: 读取 A B C D,只计算 E 的 K/V         → F
1
2
3
4
5
6
7
8
9
10
11
12

1.2 KV Cache 缓存的不是“答案” ​

KV Cache 保存的是每层 Attention 中历史 Token 的 Key/Value 张量,不是自然语言答案,也不是数据库里的问答记录。它主要优化当前序列的推理过程。

一个常用的近似公式是:

text
KV Cache bytes
≈ 2 × 层数 × Token 数 × KV Head 数 × Head Dimension × 每元素字节数
  ↑
  K 和 V 两份
1
2
3
4

以 32 层、8 个 KV Heads、Head Dimension 128、上下文 8192、FP16 为例:

text
2 × 32 × 8192 × 8 × 128 × 2 bytes
= 1,073,741,824 bytes
≈ 1 GiB / 每条序列
1
2
3

所以 KV Cache 是典型的“用显存换计算”。上下文越长、并发序列越多,显存压力越大。

1.3 Prefill 与 Decode ​

text
完整 Prompt: [System][Tools][History][User]
                       │
                       ▼
Prefill:并行处理整段 Prompt,建立初始 KV Cache
         通常计算密集,直接影响 TTFT
                       │
                       ▼
Decode:每次生成一个新 Token,并把新 K/V 追加到 Cache
        通常更受显存带宽限制,影响 TPOT
1
2
3
4
5
6
7
8
9
  • TTFT(Time To First Token):从发出请求到收到第一个 Token。
  • TPOT(Time Per Output Token):生成阶段相邻 Token 的平均时间。
  • Prompt/Prefix Cache 主要减少重复 Prefill。
  • KV Cache 主要避免每个 Decode step 重算历史 K/V。

更完整的模型内部推导见 [[/04-ai/01-llm-engineering/09-decoder-only-llm]]。本章后续重点转向 Agent 工程层。


第2部分:Prefix Cache / Prompt Cache——跨请求复用相同前缀 ​

2.1 它与 KV Cache 是什么关系? ​

可以把 Prompt Cache 理解成:**把某次 Prefill 得到的前缀状态保留下来,让后续请求复用。**底层经常复用 KV 状态,但工程语义与一次请求内部的 KV Cache 不同。

text
KV Cache:
  关注一次序列怎样逐 Token 生成
  生命周期通常跟随当前请求或会话

Prefix / Prompt Cache:
  关注多个请求之间是否拥有相同 Token 前缀
  生命周期、计费和匹配规则由推理服务决定
1
2
3
4
5
6
7

2.2 为什么强调“前缀”而不是“包含相同内容”? ​

假设两次请求分别是:

text
Request A Tokens:
[S1][S2][T1][T2][H1][U1]

Request B Tokens:
[S1][S2][T1][T2][H1][U1][R1][U2]
 └────────────完全一致的公共前缀────────────┘
1
2
3
4
5
6

B 可以复用 A 的公共前缀。如果只改变中间一个 Token:

text
Request C Tokens:
[S1][S2][T1][T9][H1][U1][R1][U2]
 └──命中──┘  X
              └────后续状态通常都要重算────┘
1
2
3
4

Attention 的后续状态依赖前面的 Token,所以变化点之后的缓存通常全部失效。相同文字出现在后面,不等于能跳过中间计算。

2.3 文本看起来一样,为什么仍可能 Miss? ​

缓存比对的是服务端实际接收到并 Tokenize 后的输入,不是人眼看到的“意思”。下面这些变化都可能改变前缀:

  • System Prompt 多一个空格、换行或时间戳。
  • JSON 属性顺序变化。
  • Tools Schema 顺序变化。
  • 工具描述动态插入环境信息。
  • 模型或 Tokenizer 版本变化。
  • 图片、音频或多模态附件变化。
  • Provider 自动注入的模板变化。
  • Compaction 用摘要替换了旧历史。
text
人眼:两个 Tool Schema 看起来等价

A = {"name":"read","type":"object"}
B = {"type":"object","name":"read"}

字节序列不同 → Token 序列可能不同 → Prefix Cache 可能失配
1
2
3
4
5
6

2.4 Coding Agent 为什么特别受益? ​

Coding Agent 每轮请求都可能携带很长且稳定的前缀:

text
┌────────────────────────────────────────────────────────────┐
│ 稳定区:适合缓存                                            │
├────────────────────────────────────────────────────────────┤
│ Provider / System 指令                                     │
│ Tool Schemas                                               │
│ 安全规则                                                   │
│ 项目级 AGENTS.md / CLAUDE.md                               │
│ 未改变的历史消息                                           │
├────────────────────────────────────────────────────────────┤
│ 动态区:放在后部                                            │
├────────────────────────────────────────────────────────────┤
│ 当前时间、工作区状态、最新 Tool Result、用户新消息          │
└────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

只要 Agent Loop 采用 Append-Only:

text
Round 1: [Stable Prefix][User 1]
Round 2: [Stable Prefix][User 1][Assistant 1][Tool 1]
Round 3: [Stable Prefix][User 1][Assistant 1][Tool 1][Assistant 2]
1
2
3

每轮都能复用上一轮的大部分前缀。反之,如果每轮重新排序工具、改写历史或把动态时间放进 System Prompt,缓存命中会从变化位置断掉。

2.5 不同模型服务的缓存接口为什么看起来不一样? ​

各家 API 都可能复用相同前缀,但产品接口通常分成三类:

形态开发者看到什么典型特点
自动前缀缓存不标记缓存点,响应中报告 cached tokens接入简单;最小长度、TTL 和路由规则由服务控制
显式 Prompt Caching在内容块上声明 cache breakpoint / cache control可控制复用边界;需要理解写入、读取和过期计费
推理引擎 Prefix Caching在 vLLM 等服务端配置中启用能控制块表、容量和驱逐;运维复杂度更高

在实践中,OpenAI 风格接口常把缓存读取量放在 usage 的输入 Token 明细里;Anthropic 风格接口会区分 cache creation 与 cache read;DeepSeek 等服务也会分别报告命中和未命中的输入 Token。字段名称、最低缓存长度、TTL、价格和隔离边界会调整,接入时必须以当前 Provider 文档为准,不能把某一家的规则当成行业标准。

json
{
  "usage": {
    "input_tokens": 52000,
    "cache_read_tokens": 41000,
    "cache_creation_tokens": 3000,
    "output_tokens": 1200
  }
}
1
2
3
4
5
6
7
8

这段示意数据表达的是:41,000 个输入 Token 复用了已有前缀,3,000 个 Token 新建了可复用状态;它不表示完整答案被缓存。真实 SDK 的字段结构应通过适配层归一化:

python
@dataclass
class NormalizedUsage:
    input_tokens: int
    cache_read_tokens: int
    cache_write_tokens: int
    output_tokens: int


def normalize_usage(provider: str, raw: dict) -> NormalizedUsage:
    """把不同 Provider 的 usage 字段转换成内部统一口径。"""
    ...
1
2
3
4
5
6
7
8
9
10
11

这样上层监控计算的是同一种 Token Cache Ratio,不会把某家的“缓存创建 Token”误当成“缓存命中 Token”。


第3部分:一次 Agent Loop 中,缓存到底在哪里命中? ​

考虑一个代码 Agent:用户要求“找到登录超时的原因并修复”。

text
Round 1
┌──────────────┬─────────────┬───────────┐
│ System+Tools │ 项目规则     │ 用户任务   │
└──────────────┴─────────────┴───────────┘
        └────── Prefill 全量计算 ──────┘
模型决定:调用 grep

Round 2
┌──────────────┬─────────────┬───────────┬────────────┐
│ System+Tools │ 项目规则     │ 用户任务   │ grep 结果  │
└──────────────┴─────────────┴───────────┴────────────┘
        └──────── cache hit ─────────┘ └─新增 Prefill─┘
模型决定:调用 read

Round 3
┌──────────────┬─────────────┬───────────┬────────────┬───────────┐
│ System+Tools │ 项目规则     │ 用户任务   │ grep 结果  │ read 结果 │
└──────────────┴─────────────┴───────────┴────────────┴───────────┘
        └──────────────── cache hit ────────────────┘ └─新增部分─┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

注意:grep/read 结果本身第一次仍要处理;命中的是它们前面的公共 Token 前缀。

3.1 Compaction 与缓存的冲突 ​

Context Engineering 会在历史太长时压缩上下文:

text
压缩前:
[System][Tools][M1][M2][M3][T1][M4][T2][M5]
 └────────────大量旧前缀可复用────────────┘

压缩后:
[System][Tools][Summary][M5]
               ↑ 新 Token 序列
1
2
3
4
5
6
7

Compaction 能减少以后每轮的输入长度,却会在发生压缩的那一轮破坏旧前缀。正确决策不是“永远不压缩”,而是比较:

text
保留长历史的未来成本
vs
一次缓存失效 + 更短上下文的未来收益
1
2
3

长任务中,主动在稳定边界做一次高质量 Compaction,通常比每轮微调摘要更缓存友好。

3.2 多 Agent 的缓存形态 ​

text
主 Agent Stable Prefix
        │
        ├── 子 Agent A:[共享规则][A 的任务][A 的历史]
        ├── 子 Agent B:[共享规则][B 的任务][B 的历史]
        └── 子 Agent C:[共享规则][C 的任务][C 的历史]
                         ↑
                   从分叉点开始不同
1
2
3
4
5
6
7

共享前缀可能被复用,但各分支的动态历史不能互相命中。为了追求命中率而把所有子 Agent 历史塞回主线程,往往会增加 Token、污染上下文,得不偿失。


第4部分:缓存命中率怎么计算——不要被一个百分比骗了 ​

4.1 请求命中率与 Token 命中率不是一回事 ​

text
Request Hit Rate = 命中缓存的请求数 / 总请求数

Token Cache Ratio = 缓存读取 Token 数 / 总输入 Token 数
1
2
3

假设 10 个请求全部“命中”,但每次只命中 100 Token、总输入 10,000 Token:

text
请求命中率 = 10 / 10 = 100%
Token 命中率 = 1,000 / 100,000 = 1%
1
2

“100% 命中”并不代表节省了 100% 的输入计算。

4.2 一个完整成本例子 ​

假设一次 Agent 任务调用模型 20 轮,每轮输入 50,000 Token,其中平均 40,000 Token 命中 Prefix Cache,10,000 Token 需要新计算:

text
总输入 Token          = 20 × 50,000 = 1,000,000
缓存读取 Token        = 20 × 40,000 =   800,000
未缓存输入 Token      = 20 × 10,000 =   200,000
Token Cache Ratio     = 800,000 / 1,000,000 = 80%
1
2
3
4

若普通输入单价为 P,缓存读取单价为 0.1P,忽略缓存写入差异:

text
无缓存成本 = 1,000,000 × P
有缓存成本 = 200,000 × P + 800,000 × 0.1P
           = 280,000 × P
节省比例   = 72%
1
2
3
4

80% Token 命中率并不等于节省80%,实际结果取决于 Provider 的缓存读取、写入和普通输入价格。

4.3 应该同时观察哪些指标? ​

指标回答的问题
cache_read_tokens多少输入 Token 从缓存读取?
cache_creation_tokens本次新写入了多少缓存?
uncached_input_tokens多少 Token 仍执行了普通 Prefill?
Token Cache Ratio大部分输入是否真正被复用?
TTFT p50/p95缓存是否改善了首 Token 延迟?
Cost per successful task缓存是否降低了完成一个任务的总成本?
Eviction / Expiration Rate缓存是否因容量或 TTL 频繁失效?
Correctness after hit命中是否返回了过期或越权结果?

最终目标应是“每个成功任务的成本和延迟下降”,而不是孤立追求命中率。


第5部分:精确响应缓存——直接复用整个答案 ​

5.1 最简单的实现 ​

python
import hashlib
import json

def canonical_json(value: dict) -> str:
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)

def response_cache_key(request: dict) -> str:
    material = {
        "model": request["model"],
        "messages": request["messages"],
        "tools": request.get("tools", []),
        "temperature": request.get("temperature", 1),
        "prompt_version": "support-agent-v7",
    }
    return hashlib.sha256(canonical_json(material).encode("utf-8")).hexdigest()

def complete_with_cache(request, cache, llm):
    key = response_cache_key(request)
    cached = cache.get(key)
    if cached is not None:
        return cached

    response = llm.complete(request)
    cache.set(key, response, ttl_seconds=300)
    return response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

关键不是 SHA-256,而是缓存键必须包含所有会影响答案的输入。

5.2 常见错误:漏掉隐式依赖 ​

下面这个键看似合理,其实危险:

python
key = hash(user_question)
1

“我的订单什么时候到?”的答案还依赖:

  • 当前用户身份与租户。
  • 订单数据库版本或更新时间。
  • System Prompt 版本。
  • 模型及推理参数。
  • 工具权限和地域。
  • 当前时间。

如果缓存键不包含用户或租户,用户 A 甚至可能命中用户 B 的订单答案,这已经不是性能 Bug,而是数据泄漏。

5.3 适用与不适用 ​

适合精确缓存:

  • 确定性的文档摘要。
  • 相同代码版本的静态分析。
  • 固定知识库版本上的 FAQ。
  • 温度为 0、输入完整、允许短时间复用的任务。

不适合直接复用:

  • 当前天气、库存、价格和余额。
  • 具有副作用的工具调用。
  • 强个性化或权限敏感回答。
  • 希望每次采样不同结果的创作任务。

第6部分:Semantic Cache——问题不同,但意思可能相同 ​

6.1 基本数据流 ​

text
新问题:“怎么重置登录密码?”
        │
        ▼ Embedding
查询向量 q
        │
        ▼ Vector Search
旧问题:“忘记密码后如何修改?”  similarity = 0.94
        │
        ├── 通过阈值、租户、时效和权限校验 → 返回旧答案
        └── 未通过 → 正常调用 LLM,并写入新缓存
1
2
3
4
5
6
7
8
9
10

伪代码:

python
def semantic_cached_answer(question, tenant_id, knowledge_version):
    vector = embed(question)
    candidates = semantic_cache.search(
        vector=vector,
        filters={
            "tenant_id": tenant_id,
            "knowledge_version": knowledge_version,
        },
        top_k=3,
    )

    best = candidates[0] if candidates else None
    if best and best.similarity >= 0.93 and not best.is_expired:
        return best.answer

    answer = run_agent(question)
    semantic_cache.insert(question, vector, answer)
    return answer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

6.2 相似不等于答案可以复用 ​

text
“怎样关闭账户?”
“怎样关闭我公司的管理员账户?”
1
2

两句话 Embedding 可能很接近,但权限和操作后果完全不同。语义缓存必须叠加结构化约束:租户、语言、产品版本、角色、地区、知识版本和时间窗口。

6.3 阈值的本质是风险权衡 ​

text
阈值太高:大量相似问题 miss,节省有限
阈值太低:错误复用旧答案,正确率下降

低风险 FAQ       可以较积极缓存
医疗/法律/财务    应提高阈值,甚至禁用直接回答缓存
有副作用的操作    不应仅凭语义相似复用执行结果
1
2
3
4
5
6

生产系统应在离线评估集上绘制“命中覆盖率—错误复用率”曲线,而不是拍脑袋选择 0.9。


第7部分:Tool Cache 与 RAG Cache ​

7.1 Tool Cache 缓存的是观察结果,不是工具调用意图 ​

text
Agent → tool: read_file(path="src/auth.ts", revision="abc123")
                     │
                     ▼
cache key = tool_version + normalized_args + data_version
                     │
          ┌──────────┴──────────┐
          │ hit                 │ miss
          ▼                     ▼
      返回旧内容             真正读文件 → 写缓存
1
2
3
4
5
6
7
8
9

对于 Coding Agent,文件路径不够成为缓存键;文件内容可能已经改变。可以加入 Git blob SHA、mtime+size 或内容哈希。

7.2 有副作用的工具不能像查询一样缓存 ​

text
安全:
get_weather(city="北京")
search_docs(query="退款")
read_file(path, revision)

危险:
send_email(to, body)
charge_card(order_id)
delete_database(name)
1
2
3
4
5
6
7
8
9

第二类工具需要的是 Idempotency Key(幂等键),不是“命中后再返回一次看起来相同的结果”。幂等键用于保证重试不会重复执行副作用:

text
request id = task-42/send-email/step-7

第一次:执行发送,记录完成
网络超时后重试:发现该 id 已完成,返回原执行结果,不再次发送
1
2
3
4

7.3 RAG 至少有四个可缓存位置 ​

text
Query
  │
  ├─① Query Embedding Cache
  ▼
Vector Search
  │
  ├─② Candidate Retrieval Cache
  ▼
Reranker
  │
  ├─③ Rerank Result Cache
  ▼
Context Assembly
  │
  └─④ Final Answer Cache(可选、风险最高)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

缓存键必须绑定索引版本:

python
key = hash({
    "query": normalized_query,
    "embedding_model": "text-embedding-v4",
    "index_version": "kb-2026-07-29-03",
    "filters": {"tenant": tenant_id, "language": "zh-CN"},
    "top_k": 20,
})
1
2
3
4
5
6
7

如果知识库更新但 index_version 没变,旧检索结果会继续命中,用户就会看到“明明文档改了,Agent 仍回答旧内容”。


第8部分:缓存失效——两件最难的事之一 ​

8.1 四种常见策略 ​

策略做法优点缺点
TTL到时间自动过期简单、通用过期前可能旧,过期后可能浪费
Versioned Key键中加入 Prompt/数据/模型版本精确、易回滚需要管理版本传播
Event Invalidation数据变化时主动删除相关键新鲜度高事件链复杂,容易漏删
Content Addressing用内容哈希作为键内容不变即可永久复用不适合依赖隐式外部状态的结果

生产系统通常组合使用:

text
cache key:
tenant / agent-version / model / tool-version / data-version / input-hash

再附加:
TTL + 容量淘汰 + 数据变更事件
1
2
3
4
5

8.2 Stampede:热点键同时失效 ​

text
10,000 个请求 ──同时查询──> 热点缓存键(刚过期)
       │
       └──10,000 次全部 miss──> 10,000 次模型/API 调用
1
2
3

常见解决方法:

  • Singleflight:同一 key 只允许一个请求回源,其他等待。
  • TTL Jitter:给过期时间加入随机抖动,避免同时失效。
  • Stale-While-Revalidate:短时间返回旧值,后台刷新。
  • 提前刷新:热点键接近过期时异步更新。

8.3 Negative Cache ​

如果检索或工具反复查询不存在的数据,也可以短暂缓存“未找到”:

text
get_order("ORD-NOT-EXIST") → 404
缓存 30 秒的 NOT_FOUND
1
2

但 Negative Cache 的 TTL 应更短,否则数据刚创建后仍会被旧的“不存在”遮住。


第9部分:缓存安全——命中错误比未命中更危险 ​

9.1 租户隔离必须进入缓存键 ​

text
错误:cache:{question_hash}

正确:cache:{tenant_id}:{user_scope}:{policy_version}:{question_hash}
1
2
3

缓存层不能绕开源系统权限。即使两个用户提出相同问题,也不代表他们有权看到相同结果。

9.2 不要缓存敏感原文 ​

Prompt Cache、日志和应用缓存中可能出现:

  • API Key、Cookie、OAuth Token。
  • 客户代码与商业机密。
  • PII、医疗和财务信息。
  • Tool Result 中的数据库记录。

设计前必须确认 Provider 的保留策略、加密方式、区域、组织隔离和 Zero Data Retention 条件。应用缓存则应使用最小化数据、加密、访问控制和审计。

9.3 Cache Poisoning ​

text
攻击者构造恶意问题
        ↓
系统生成了带错误指令的答案
        ↓
语义缓存把它标记为可复用
        ↓
正常用户提出相似问题并命中恶意答案
1
2
3
4
5
6
7

只有经过验证、来源可信、权限范围明确的输出才应进入共享语义缓存。用户私有内容默认只能进入用户或租户隔离的缓存空间。


第10部分:缓存友好的 Agent 上下文设计 ​

10.1 推荐布局:稳定内容在前,动态内容在后 ​

text
┌──────────────────────────────────────────────────────────────┐
│ Layer 1:Provider / System 基础指令        极稳定             │
├──────────────────────────────────────────────────────────────┤
│ Layer 2:Tools Schema 与安全策略           稳定               │
├──────────────────────────────────────────────────────────────┤
│ Layer 3:项目规则、Skill、固定参考材料      较稳定             │
├──────────────────────────────────────────────────────────────┤
│ Layer 4:Append-Only 对话与工具历史         持续追加           │
├──────────────────────────────────────────────────────────────┤
│ Layer 5:当前时间、工作区状态、最新请求     高动态             │
└──────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11

这并不是说所有稳定内容都应该永久塞进 Prompt。无关内容仍应按需加载;缓存能降低重复计算成本,但不能消除上下文污染、注意力稀释和 KV 显存占用。

10.2 十条工程规则 ​

  1. 固定 System Prompt 模板,不在开头插入每轮时间戳。
  2. 对 Tool Schema 做确定性排序和规范化序列化。
  3. 使用明确的 Prompt Version,而不是原地悄悄修改。
  4. 对话历史优先 Append-Only,不随意重写中间消息。
  5. 动态状态放在靠后位置,并标明数据版本。
  6. Compaction 在清晰检查点批量进行,不要每轮微调摘要。
  7. Tool/RAG 键包含工具、模型、索引和权限版本。
  8. 对副作用使用幂等键,不把普通缓存当事务系统。
  9. 同时监控命中率、TTFT、正确率和任务总成本。
  10. 缓存失败必须能安全回源;缓存不是单点真相来源。

10.3 一个常见反模式 ​

python
system_prompt = f"""
你是代码助手。
当前时间:{datetime.now()}
当前分支:{git_branch()}
当前未提交文件:{git_status()}
下面是固定安全规则……
"""
1
2
3
4
5
6
7

时间位于最前面,每轮都会变化,后续所有固定规则和工具定义都可能无法复用前缀。更合理的结构是:

text
[固定 System Prompt]
[固定 Tools]
[固定安全规则]
[历史消息]
[Current Environment Snapshot: timestamp, branch, status]
[本轮用户请求]
1
2
3
4
5
6

第11部分:如何排查“缓存命中率突然下降” ​

11.1 排障树 ​

text
缓存命中率下降
│
├─ 输入前缀变了吗?
│  ├─ System Prompt 发布新版本
│  ├─ Tools 顺序或 JSON 序列化不稳定
│  ├─ 动态时间/随机 ID 被放到前部
│  └─ Compaction 改写了历史
│
├─ 服务边界变了吗?
│  ├─ 模型或 Tokenizer 切换
│  ├─ Provider / Region / Project 切换
│  ├─ Cache TTL 到期
│  └─ 容量压力导致驱逐
│
├─ 指标口径变了吗?
│  ├─ 按请求统计改成按 Token 统计
│  ├─ 流量结构变化,新会话占比升高
│  └─ 短请求增多,低于最小缓存门槛
│
└─ Agent 行为变了吗?
   ├─ Tool Result 变长
   ├─ 更多分支和子 Agent
   └─ 重试导致大量不同前缀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

11.2 给每次请求记录“前缀指纹” ​

不要把完整 Prompt 打进日志。可以对结构化片段分别计算哈希:

json
{
  "model": "model-x",
  "system_hash": "sha256:...",
  "tools_hash": "sha256:...",
  "project_rules_hash": "sha256:...",
  "prompt_version": "agent-v12",
  "input_tokens": 52140,
  "cache_read_tokens": 43820,
  "cache_creation_tokens": 8320,
  "ttft_ms": 740
}
1
2
3
4
5
6
7
8
9
10
11

当命中率下降时,可以快速发现是 tools_hash 每轮都变,还是 Prompt 发布导致所有旧缓存自然冷启动。

11.3 冷启动不是故障 ​

新版本刚部署、模型刚切换、缓存刚扩容时,命中率短暂下降很正常。应该观察从冷到热的恢复曲线,而不是看到一分钟的低命中率就回滚。


第12部分:面试高频问答 ​

Q1:KV Cache 和 Prompt Cache 有什么区别? ​

标准回答:KV Cache 是 Transformer 自回归推理中保存历史 Token 的 K/V 张量,避免每个 Decode step 重算历史;Prompt/Prefix Cache 是跨请求复用相同 Token 前缀的 Prefill 状态。前者解释“一次生成为什么能高效继续”,后者解释“多次相似请求为什么能跳过重复前缀计算”。

Q2:为什么 Prefix Cache 要求前缀一致? ​

标准回答:Causal Attention 中每个位置的状态依赖它之前的 Token。一旦中间 Token 变化,后续位置的 Attention 输入也随之变化,因此变化点之后的中间状态通常不能继续复用。

Q3:为什么 Append-Only 有利于 Coding Agent? ​

标准回答:每轮只在旧 messages 后追加 assistant、tool 和 user 消息,旧 Token 前缀保持不变,服务可以复用上一轮的大部分 Prefill 状态。若重写中间历史,变化点之后的缓存会失效。

Q4:缓存命中是否代表模型不会被调用? ​

标准回答:不一定。响应缓存或语义答案缓存命中时可能不调用模型;Prompt Cache 命中时模型仍需处理新增输入并完成 Decode,只是复用了公共前缀计算。

Q5:如何设计 LLM 响应缓存键? ​

标准回答:包含所有影响输出和权限的因素,例如模型、消息、工具、推理参数、Prompt 版本、数据版本、租户和权限范围;使用规范化序列化后再哈希。不能只用用户问题作为键。

Q6:TTL 越长越好吗? ​

标准回答:不是。TTL 越长通常命中越高,但陈旧数据和安全风险也越大。TTL 应由数据变化速度、错误代价、回源成本和主动失效能力决定。

Q7:语义缓存最大的风险是什么? ​

标准回答:语义相似不代表答案可复用,可能跨用户、跨权限、跨产品版本返回错误或敏感答案。因此必须加入租户、权限、版本、时效过滤,并用评估集校准相似度阈值。

Q8:缓存与幂等有什么区别? ​

标准回答:缓存用于避免重复计算或读取;幂等保证重复执行请求不会产生额外副作用。查询工具可使用缓存,支付、发邮件等副作用工具需要幂等键和持久化执行记录。

Q9:怎样提高 Prompt Cache 命中率? ​

标准回答:稳定 System Prompt 和 Tools Schema,把静态内容放前、动态内容放后,确定性排序 JSON 和工具,采用 Append-Only 历史,在明确检查点做 Compaction,并避免无意义的模型、区域和配置切换。

Q10:命中率很高,为什么成本仍然很高? ​

标准回答:可能只统计了请求命中而不是 Token 命中;输出 Token、Tool/API、缓存写入仍然收费;上下文过长仍占 KV 显存;Agent 轮次和重试过多。应该看每个成功任务总成本,而非单一百分比。


第13部分:自测题 ​

测试1:识别缓存层 ​

模型在同一次生成中,生成第 500 个 Token 时不再重新计算前 499 个 Token 的 K/V。这是哪一种缓存?

  • A. Semantic Cache
  • B. Prompt Cache
  • C. KV Cache
  • D. Tool Cache

测试2:前缀变化 ​

System Prompt、Tools 和前 20 轮消息都没有变化,只在末尾追加一条 Tool Result。哪些内容最有机会命中 Prefix Cache?

测试3:安全 ​

客服 Agent 使用 hash(question) 作为语义缓存键。为什么即使相似度判断完全准确,它仍可能泄漏数据?

测试4:成本计算 ​

某任务输入 500,000 Token,其中 350,000 是缓存读取。普通输入价格为 P,缓存读取价格为 0.2P。忽略写入和输出,成本相对无缓存降低多少?

测试5:工程选择 ​

下面哪项最不利于 Prompt Cache?

  • A. Tool Schema 按工具名稳定排序
  • B. 每轮在 System Prompt 开头写入当前毫秒时间戳
  • C. 只在历史末尾追加 Tool Result
  • D. Prompt 发布时显式修改版本号

测试6:Tool Cache ​

为什么 read_file(path) 的缓存键通常还需要文件版本或内容哈希?

测试7:Compaction ​

Compaction 明明缩短了上下文,为什么执行 Compaction 的那一轮仍可能变慢?


第14部分:自测答案 ​

测试1答案 ​

**C,KV Cache。**它保存历史 Token 在各 Attention 层的 K/V,避免 Decode 时重复计算。

测试2答案 ​

从 System Prompt 开始,到旧历史的最后一个 Token 为止,都是与上一轮完全一致的公共前缀,有机会命中。新追加的 Tool Result 需要执行新的 Prefill。

测试3答案 ​

缓存键没有租户、用户身份和权限范围。两个用户可以提出语义完全相同的问题,但有权看到的数据不同。用户 B 可能命中用户 A 基于私有订单生成的答案。

测试4答案 ​

text
无缓存成本 = 500,000P
有缓存成本 = 150,000P + 350,000 × 0.2P
           = 220,000P
降低比例   = (500,000 - 220,000) / 500,000
           = 56%
1
2
3
4
5

测试5答案 ​

**B。**最前面的时间戳每轮都变,使公共 Token 前缀几乎从开头就断裂。动态信息应放到稳定前缀之后。

测试6答案 ​

相同路径的文件内容可能已经变化。若只用路径作键,Agent 会读取旧内容并基于旧代码推理。Git blob SHA、revision 或内容哈希可以把缓存绑定到具体内容版本。

测试7答案 ​

Compaction 用新的 Summary Token 替换旧历史,破坏了之前的公共前缀,因此服务需要为新序列重新 Prefill。之后各轮因为上下文更短、且新摘要可形成稳定前缀,才逐渐获得收益。


核心总结 ​

  1. 先问哪一层缓存。 KV Cache、Prompt Cache、响应缓存、语义缓存和 Tool Cache 复用的是完全不同的工作。
  2. **Prefix Cache 复用计算,不等于复用答案。**模型仍需处理新增 Token 并生成新输出。
  3. **缓存命中依赖 Token 前缀稳定。**稳定内容在前、动态内容在后、Append-Only 历史和确定性 Tool Schema 是 Coding Agent 的关键设计。
  4. **缓存键必须表达全部依赖。**模型、Prompt、工具、数据、租户、权限和版本遗漏任何一项,都可能产生陈旧结果或数据泄漏。
  5. **不要迷信命中率。**同时观察 Token 命中、TTFT、正确率和每个成功任务的总成本。
  6. **失效与安全比写入更难。**TTL、版本化、事件失效、租户隔离和防缓存污染必须从设计阶段进入系统。

相关笔记 ​

  • [[14-context-engineering]] - 上下文由哪些信息组成,以及如何压缩与编排
  • [[15-harness-loop-skills]] - 如何在 Context 外层建立验证、执行和持续循环
  • [[04-memory-management]] - Agent 的短期、长期与外部记忆
  • [[07-rag-advanced]] - 混合检索、重排与 RAG 数据流
  • [[27-coding-agents-comparison]] - Reasonix 的 Cache-First 与 Append-Only 设计
  • [[/04-ai/01-llm-engineering/09-decoder-only-llm]] - KV Cache 的模型内部原理

下一步学习 ​

  • [ ] 阅读 15 - Harness、Loop 与 Skills
  • [ ] 为一个 Agent 请求记录 system_hash、tools_hash、缓存 Token 和 TTFT
  • [ ] 对比 Append-Only 与每轮重写 System Prompt 时的 Token 缓存命中率
  • [ ] 为一个只读 Tool 设计包含版本、租户和规范化参数的缓存键

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇15. Context Engineering - 从 Prompt 设计到上下文编排 / Context Engineering: From Prompt Design to Context Orchestration
下一篇17. Harness Engineering, Skills, and Loop Engineering — 从信任模型到验证系统 / From Trusting Models to Verifying Systems

持续记录,持续成长

Copyright © Tidenflow