在 Dify 开发智能体时,我们通常会配置一段固定的 System Prompt,包括角色定义、业务规则、工具使用规范等。
用户每发起一轮对话,Dify 都需要向模型发送 System Prompt、相关历史对话和最新问题。
这就产生了一个问题:
既然 System Prompt 和部分历史对话是重复的,模型能不能直接复用之前的计算结果?
这正是 Prompt Cache(上下文缓存)要解决的问题。
一、从 Dify 多轮对话理解 Prompt Cache
假设我们使用 Dify + DeepSeek 开发一个客服智能体。
System Prompt 固定为:
你是一个电商客服。
回答必须简洁、礼貌。
不能编造订单信息。
用户连续进行三轮对话,假设 Dify 保留了完整历史记录,实际发送给模型的消息可以简化为:
第一次请求:
System:固定客服规则
User:你好
第二次请求:
System:固定客服规则
User:你好
Assistant:你好,有什么可以帮你?
User:我的订单在哪里?
第三次请求:
System:固定客服规则
User:你好
Assistant:你好,有什么可以帮你?
User:我的订单在哪里?
Assistant:请提供订单编号。
User:订单号是123456
抽象一下:
第1次:System + Q1
第2次:System + Q1 + A1 + Q2
第3次:System + Q1 + A1 + Q2 + A2 + Q3
可以发现:后一次请求的开头,包含了前一次请求已经处理过的内容。
这部分连续相同的内容称为公共前缀(Common Prefix)。
模型平台可以缓存公共前缀对应的计算结果,后续请求直接复用,减少重复计算。
需要注意:缓存不代表后续请求不再发送 System Prompt,而是模型服务端复用此前的计算结果。
二、Prompt Cache 的底层原理
2.1 Prefill 与 Decode
大模型推理主要包含两个阶段:
- Prefill(预填充):处理输入 Token,执行 Transformer 计算,构建注意力相关状态。
- Decode(解码):根据已有上下文,逐个生成回答 Token。
其过程可以简化为:
System + History + Question
|
v
Prefill
|
v
KV Cache
|
v
Decode
|
v
模型回答
对于较长的 System Prompt,每次重新进行 Prefill 都会消耗 GPU 计算资源。
Prompt Cache 的目的,就是减少相同前缀的重复计算。
2.2 KV Cache 是什么?
Transformer 的 Attention 机制涉及三个核心概念:
- Query(Q):当前 Token 要关注什么。
- Key(K):用于匹配注意力的信息。
- Value(V):用于聚合的信息。
KV Cache 保存的是模型已经计算完成的 Key 和 Value 中间状态,而不是原始文本或模型回答。
例如:
第一次输入:A + B + C
计算 A、B、C 对应的 K/V
保存缓存
第二次输入:A + B + C + D
复用 A、B、C 的 K/V
计算新增的 D
为什么可以复用?
因为自回归 Transformer 使用 Causal Attention(因果注意力):
前面的 Token 无法关注未来的 Token。
因此,新增 D 不会改变已经计算完成的 A、B、C 的历史 K/V 状态。
这就是前缀缓存成立的底层原因。
2.3 Prompt Cache 与普通缓存的区别
|---------------|------------------------|-----------------|
| 缓存类型 | 缓存内容 | 作用 |
| Redis / 应用层缓存 | Prompt 文本、用户配置 | 避免重复构建或读取 |
| KV Cache | Transformer 的 K/V 中间状态 | 减少推理过程中的重复计算 |
| Prompt Cache | 跨请求复用的前缀计算状态 | 降低重复 Prefill 成本 |
Prompt Cache 缓存的是计算结果,不是最终答案。
三、DeepSeek 的上下文缓存机制
DeepSeek 官方 API 提供 Context Caching on Disk(上下文硬盘缓存),默认启用,无需额外配置。
其工作流程可以简化为:
Dify 发起请求
|
v
DeepSeek 检查公共前缀
|
+-- 命中缓存
| |
| v
| 读取已缓存的状态
|
+-- 未命中
|
v
GPU 计算
|
v
处理新增内容
|
v
生成回答
3.1 为什么使用硬盘缓存?
GPU 显存容量有限且成本高,无法长期保存大量用户的全部 KV Cache。
DeepSeek 通过硬盘持久化可复用的前缀状态,减少重复 GPU 计算,同时降低缓存存储成本。
3.2 为什么第二次不一定命中?
DeepSeek 会在请求边界、公共前缀以及一定的 Token 间隔处创建缓存单元。
例如:
第一次:A + B
第二次:A + C
第三次:A + D
第一次可能缓存 A+B。
第二次虽然与第一次有公共前缀 A,但不一定存在可独立匹配的 A 缓存单元。
当平台识别并保存 A 之后,第三次才可能命中。
因此,相同前缀不保证一定命中缓存,还取决于缓存单元是否已经建立并且仍然有效。
四、Dify + DeepSeek 缓存验证
4.1 Dify 需要开启缓存吗?
如果 Dify 直接使用 DeepSeek 官方 API,通常不需要单独配置,DeepSeek 会自动处理上下文缓存。
但是是否真正命中,需要检查 API 返回的统计数据。
DeepSeek 提供两个重要字段:
{
"usage": {
"prompt_tokens": 4500,
"prompt_cache_hit_tokens": 4000,
"prompt_cache_miss_tokens": 500
}
}
以上为示例数据。
prompt_cache_hit_tokens:命中缓存的输入 Token 数量。prompt_cache_miss_tokens:未命中缓存的输入 Token 数量。
缓存命中率:
命中率 = 缓存命中Token / 总输入Token × 100%
4.2 Python 验证示例
可以使用 DeepSeek 官方 OpenAI 兼容接口进行测试。
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com"
)
system_prompt = (
"你是一个客服,回答必须准确、简洁。\n"
) * 200
messages = [
{"role": "system", "content": system_prompt}
]
for question in ["你好", "你能做什么", "帮我查询订单"]:
messages.append({
"role": "user",
"content": question
})
response = client.chat.completions.create(
model="deepseek-flash",
messages=messages,
extra_body={"thinking": {"type": "disabled"}}
)
usage = response.usage
print("缓存命中:", usage.prompt_cache_hit_tokens)
print("缓存未命中:", usage.prompt_cache_miss_tokens)
messages.append({
"role": "assistant",
"content": response.choices[0].message.content
})
这段代码用于验证 DeepSeek API 的缓存能力。如果要验证 Dify 是否实际命中,还需要获取 Dify 请求对应的 DeepSeek 原始 usage 数据。
4.3 哪些操作会破坏缓存?
Prompt Cache 依赖从请求开头开始连续相同的前缀。
例如:
第一次:
System:你是一个电商客服。
第二次:
System:你是一个专业电商客服。
仅修改了开头几个字,就会影响公共前缀匹配。
常见影响因素包括:
- 修改 System Prompt 开头。
- 在固定规则前插入当前时间、随机 UUID。
- 每轮重新生成历史摘要。
- 截断最早的历史消息。
- 改变工具定义或运行时变量的位置。
其中,变化越靠前,可能损失的可复用前缀越多。
但前缀变化不等于删除已有缓存,也不保证缓存命中 Token 一定归零。
五、Agent 中的 Prompt 缓存优化
工业级 Agent 的 System Prompt 可能包含几千个 Token,且不同内容的变化频率不同。
因此,可以按照变化频率划分三个区域。
|--------|-----------------|----------|
| 分区 | 内容 | 处理方式 |
| 静态区 | 角色定义、安全策略、工具规则 | 全局复用 |
| 半静态区 | 用户偏好、长期记忆 | 按用户和版本缓存 |
| 动态区 | 当前时间、临时指令、运行时信息 | 每轮生成 |
推荐排列:
System Prompt
│
├── 静态区
│ ├── 角色规则
│ └── 安全策略
│
├── 半静态区
│ ├── 用户偏好
│ └── 长期记忆
│
└── 动态区
├── 当前环境
└── 临时指令
核心原则:
静态前置、动态后置,最大化前缀缓存的复用机会。
应用层可以使用 lru_cache 或 Redis 缓存静态配置,避免重复构建 Prompt。
模型侧则通过 Prompt Cache 复用已经计算的前缀状态。
有些 Agent 项目会使用 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 标记静态与动态内容的边界,但这只是应用代码中的组织约定,不会自动触发 DeepSeek 缓存。
实际生产环境还需要考虑配置版本、缓存失效、用户隔离,以及 Dify 最终消息结构对前缀稳定性的影响。
六、总结
通过 Dify + DeepSeek 的实践,可以得出几个核心结论:
- Prompt Cache 基于相同前缀复用计算结果,不仅 System Prompt,历史对话也可能成为缓存内容。
- 底层基础是 KV Cache 和 Causal Attention,已经计算完成的前缀 K/V 可以用于后续请求。
- DeepSeek 默认支持上下文硬盘缓存,但相同前缀不保证每次都命中。
- 通过 prompt_cache_hit_tokens****可以验证缓存效果,不能仅凭 System Prompt 不变就认定缓存成功。
- Agent 应尽量让稳定内容位于 Prompt 前部,减少动态内容对前缀匹配的影响。
最终,Prompt Cache 的核心价值可以概括为:
请求仍然携带完整上下文,但模型可以复用相同前缀已经完成的计算,减少 GPU 开销、降低输入成本,并有机会缩短响应延迟。
