# 提示缓存与会话

URL: https://docs.router.ezat.io/zh/guides/prompt-caching

> 提示缓存如何在 EZAT Router 上帮你省钱、如何在两种 API 上使用，以及为什么一个对话会固定在同一个部署上。

## 何时阅读本页

你会多次发送同一段很长的前缀（系统提示词、文档、工具定义，或不断增长的对话），希望为它少付些费用。本页介绍缓存有效期、Anthropic 格式 API 上的 `cache_control`、OpenAI 兼容 API 上的自动缓存、会话，以及如何确认缓存生效。

## 缓存能省什么

提示缓存（prompt caching）保存的是提示词开头部分处理后的结果。当下一个请求以完全相同的内容开头时，模型会从缓存中读取这部分，而不是重新处理。

| token 类型 | 价格 |
| --- | --- |
| 输入 | 模型的输入价格 |
| 缓存写入 | 高于输入价格。1 小时缓存的写入价格高于 5 分钟缓存。 |
| 缓存读取 | 远低于输入价格 |

各模型的价格见[价格](https://docs.router.ezat.io/zh/pricing.md)。缓存的前缀只要被读回一两次就划算了。写入后再也没被读取的前缀，比不缓存直接发送更贵。

- **缓存匹配的是完全相同的前缀**，顺序为 `tools`、`system`、`messages`，一直到标有 `cache_control` 的块为止。在这之前任何地方的改动（系统提示词中的时间戳、调整了顺序的工具）都会导致缓存未命中。
- **短提示词不会被缓存**。每个模型都有最小可缓存长度（几百到几千个 token）。低于这个长度时，请求照常执行，按普通输入计费，不会报错。
- **每次读取都会刷新有效期**，且不额外收费。

## 缓存有效期

| 有效期 | `cache_control` | 适用场景 |
| --- | --- | --- |
| 5 分钟（默认） | `{"type": "ephemeral"}` | 请求之间相隔几分钟以内：聊天、agent 循环、连续执行的任务 |
| 1 小时 | `{"type": "ephemeral", "ttl": "1h"}` | 请求之间的间隔超过 5 分钟 |

有效期从写入或最近一次读取该缓存条目的请求开始时算起。[模型](https://docs.router.ezat.io/zh/models.md)页面的「能力」一列显示哪些模型提供 1 小时缓存。

## 在 Anthropic 格式的 API 上

把 `cache_control` 放在你想缓存的前缀的最后一个块上：`tools` 中的最后一个工具、`system` 中的一个块，或 `messages` 中的一个内容块。每个请求最多 4 个断点。

```python tab="Python" test
import os

import anthropic

client = anthropic.Anthropic(base_url="https://api.router.ezat.io", api_key=os.environ["EZAT_API_KEY"])

# A long, unchanging system prompt: the part worth caching.
system = [
    {
        "type": "text",
        "text": "You answer questions about the Example Ltd product manual. " + "Manual text. " * 400,
        "cache_control": {"type": "ephemeral"},
    }
]

for question in ["How do I reset my password?", "How do I change my email address?"]:
    message = client.messages.create(
        model="anthropic/claude-sonnet-5-5",
        max_tokens=64,
        system=system,
        messages=[{"role": "user", "content": question}],
        # Different questions are different sessions; one session id keeps them on one deployment.
        extra_headers={"x-ezat-session": "manual-assistant-v1"},
    )
    usage = message.usage
    print("write", usage.cache_creation_input_tokens, "read", usage.cache_read_input_tokens)
```

只要前缀超过模型的最小长度，第一个请求会写入缓存，第二个请求会读取缓存。

Anthropic 的顶层 `cache_control`（请求上的一个字段，由它替你放置断点）同样会透传，但 Claude Opus 4.6 会拒绝它。在块上设置的断点对所有支持缓存的模型都有效。

## 在 OpenAI 兼容 API 上

OpenAI 的格式无法标记断点，所以 EZAT 会自动缓存。对于自己没有设置断点、且发往支持缓存的模型的请求，EZAT 会放置两个 5 分钟的断点：

1. 在稳定前缀之后：系统提示词，没有系统提示词时为最后一个工具；
2. 在最新的消息上，供对话的下一轮读取。

这样，多轮对话中已经发送过的内容都会从缓存读取。

- **关闭缓存**：对单个请求使用请求头 `x-ezat-cache: off`。超过最小长度、且不会再次发送的一次性提示词应当这样做：否则它们要为一次永远不会被读取的缓存写入付费。
- **自己放置断点**：像 OpenRouter 一样，在内容部分（`text`、`image_url`、`file`，或系统消息的文本部分）或工具上加上 `cache_control`。这时 EZAT 不再添加断点。`ttl` 字段在这里同样有效。
- **`prompt_cache_key`** 为会话命名，作用与 `x-ezat-session` 相同（见下文）。

```json
{
  "model": "anthropic/claude-sonnet-5-5",
  "messages": [
    {
      "role": "system",
      "content": [
        { "type": "text", "text": "Long instructions…", "cache_control": { "type": "ephemeral", "ttl": "1h" } }
      ]
    },
    { "role": "user", "content": "First question" }
  ]
}
```

## 会话：一个对话，一个部署

EZAT 用多个部署提供每个模型，而提示缓存只存在于其中一个部署中。发往另一个部署的请求会错过缓存，并重新写入。所以 EZAT 会把同一个对话的请求保持在服务过它的部署上。

- **会话**由你的组织、系统提示词和第一条消息共同确定，忽略 `cache_control`（客户端每一轮都会移动断点）。正常对话的每一轮都属于同一个会话，你无需做任何事。
- **EZAT 会记住这个部署 1 小时**，从会话最后一次成功请求算起。
- **会话会在以下情况迁移**：它的部署不健康、容量不足，或停止服务。之后的下一个请求会在新部署上重新写入缓存。

自动确定的会话不合适时，请自己为会话命名：

| 情况 | 发送什么 |
| --- | --- |
| 对话过程中系统提示词或第一条消息会变化（系统提示词中带日期、历史记录被裁剪或摘要） | 每个对话一个 id |
| 许多彼此独立的请求共享一段很长的前缀（同一个系统提示词、相同的文档） | 每个共享前缀一个 id，如上面的示例 |
| 其他情况 | 什么都不用发 |

| API | 方式 |
| --- | --- |
| 两者 | 请求头 `x-ezat-session: <id>`。任意字符串；只有前 256 个字符有效。 |
| OpenAI 兼容 | 请求体字段 `prompt_cache_key`，最多 256 个字符（更长会以 `400` 拒绝）。两者都发送时以请求头为准。 |

会话 id 只在你自己组织的请求范围内分组。不要给所有流量都发送同一个 id：只要那个部署还有容量，所有流量都会发往它。

## 确认缓存生效

| 位置 | 缓存写入 | 缓存读取 |
| --- | --- | --- |
| Anthropic 格式的 `usage` | `cache_creation_input_tokens`（提供方报告时，在 `cache_creation` 中按有效期细分） | `cache_read_input_tokens` |
| OpenAI 格式的 `usage` | `prompt_tokens_details.cache_write_tokens` | `prompt_tokens_details.cached_tokens` |
| 控制台「用量」页面 | 缓存（写入 / 读取） | 缓存（写入 / 读取） |

在 Anthropic 格式的 API 上，`input_tokens` 只统计未缓存的输入；输入总量为 `input_tokens + cache_creation_input_tokens + cache_read_input_tokens`。在 OpenAI 兼容 API 上，`prompt_tokens` 已经是总量。

如果缓存读取一直为零：

- 前缀在请求之间发生了变化，或低于模型的最小长度；
- 请求之间相隔超过 5 分钟（请使用 1 小时有效期）；
- 请求属于不同的会话（请发送 `x-ezat-session`）；
- 模型不支持缓存（见下一节）。

## 有特殊情况的模型

| 模型 | 有何不同 |
| --- | --- |
| `capabilities.promptCaching` 为 `false` 的模型（目前：Claude Fable 5） | 它们的缓存永远读不回来，所以在两种 API 上，EZAT 都会从发往它们的请求中移除 `cache_control`，并跳过自动缓存。你按普通输入付费，而不必为永远不会被读取的缓存写入付费。`usage` 中不显示缓存 token。 |
| Claude Opus 4.6 | 拒绝 Anthropic 的顶层 `cache_control`。请在块上设置断点。 |

每个模型的 `capabilities` 见 `GET https://api.router.ezat.io/api/models`（无需 key）。[模型](https://docs.router.ezat.io/zh/models.md)页面的「能力」一列会显示「提示缓存」和「1 小时缓存」。

## Claude Code

Claude Code 会自己放置断点，而且它的系统提示词和第一条消息在整个对话中保持不变，所以缓存和会话无需任何设置即可生效。见 [Claude Code](https://docs.router.ezat.io/zh/tools/claude-code.md)。

## 给 AI agent 的说明

1. Anthropic 格式 API：在稳定前缀的最后一个工具、系统块或消息块上放置 `{"type": "ephemeral"}`（或加上 `"ttl": "1h"`）。最多 4 个断点。
2. OpenAI 兼容 API：缓存是自动的；除非需要 1 小时有效期，否则不要添加断点。一次性提示词发送 `x-ezat-cache: off`。
3. 对于一个对话，如果它的系统提示词或第一条消息会变化，每一轮都发送相同的 `x-ezat-session`（或 `prompt_cache_key`）。不要所有流量都用同一个值。
4. 在第二个请求上检查 `cache_read_input_tokens`（Anthropic 格式）或 `prompt_tokens_details.cached_tokens`（OpenAI 格式），确认缓存读取生效。