从 Dify 实践理解大模型上下文缓存

在 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:你是一个专业电商客服。

仅修改了开头几个字,就会影响公共前缀匹配。

常见影响因素包括:

  1. 修改 System Prompt 开头。
  2. 在固定规则前插入当前时间、随机 UUID。
  3. 每轮重新生成历史摘要。
  4. 截断最早的历史消息。
  5. 改变工具定义或运行时变量的位置。

其中,变化越靠前,可能损失的可复用前缀越多。

但前缀变化不等于删除已有缓存,也不保证缓存命中 Token 一定归零。


五、Agent 中的 Prompt 缓存优化

工业级 Agent 的 System Prompt 可能包含几千个 Token,且不同内容的变化频率不同。

因此,可以按照变化频率划分三个区域。

|--------|-----------------|----------|
| 分区 | 内容 | 处理方式 |
| 静态区 | 角色定义、安全策略、工具规则 | 全局复用 |
| 半静态区 | 用户偏好、长期记忆 | 按用户和版本缓存 |
| 动态区 | 当前时间、临时指令、运行时信息 | 每轮生成 |

推荐排列:

复制代码
System Prompt
│
├── 静态区
│   ├── 角色规则
│   └── 安全策略
│
├── 半静态区
│   ├── 用户偏好
│   └── 长期记忆
│
└── 动态区
    ├── 当前环境
    └── 临时指令

核心原则:

静态前置、动态后置,最大化前缀缓存的复用机会。

应用层可以使用 lru_cache 或 Redis 缓存静态配置,避免重复构建 Prompt。

模型侧则通过 Prompt Cache 复用已经计算的前缀状态。

有些 Agent 项目会使用 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 标记静态与动态内容的边界,但这只是应用代码中的组织约定,不会自动触发 DeepSeek 缓存。

实际生产环境还需要考虑配置版本、缓存失效、用户隔离,以及 Dify 最终消息结构对前缀稳定性的影响。


六、总结

通过 Dify + DeepSeek 的实践,可以得出几个核心结论:

  1. Prompt Cache 基于相同前缀复用计算结果,不仅 System Prompt,历史对话也可能成为缓存内容。
  2. 底层基础是 KV Cache 和 Causal Attention,已经计算完成的前缀 K/V 可以用于后续请求。
  3. DeepSeek 默认支持上下文硬盘缓存,但相同前缀不保证每次都命中。
  4. 通过 prompt_cache_hit_tokens****可以验证缓存效果,不能仅凭 System Prompt 不变就认定缓存成功。
  5. Agent 应尽量让稳定内容位于 Prompt 前部,减少动态内容对前缀匹配的影响。

最终,Prompt Cache 的核心价值可以概括为:

请求仍然携带完整上下文,但模型可以复用相同前缀已经完成的计算,减少 GPU 开销、降低输入成本,并有机会缩短响应延迟。

相关推荐
小羊没烦恼!1 小时前
关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁
java·服务器·网络·学习·负载均衡
考虑考虑2 小时前
瀚高数据库
数据库·后端·postgresql
Fuyo_11192 小时前
C++ AVL树保姆级详解:从平衡因子到四大旋转机制
java·c++·算法
锋行天下3 小时前
DeepSeek写Langgraph就是个大傻逼,一本正经胡说八道
前端·人工智能·后端
Sarvartha3 小时前
Date、LocalDate、LocalTime、LocalDateTime、DateTimeFormatter类
java
阿俊-全栈开发3 小时前
LikeShop 多商户 Java 版商城微服务模块拆分与部署架构演进实践
java·微服务·架构·likeshop开源商城·多商户源码
Wang's Blog3 小时前
Java 项目实战:交友项目 - 联系人列表
java·开发语言·交友
我命由我123454 小时前
访客系统形式(有用户概念、无用户概念)
java·开发语言·前端·javascript·python·产品运营·产品经理
香蕉大香蕉(06敬畏市场)4 小时前
MySQL 单点详解(01)
java·数据库·mysql·面试·博客·实习