如果你使用大型语言模型已经有一段时间了,肯定会遇到两个主要痛点:成本和延迟。处理每个token都需要花钱,如果重复发送相同的上下文(比如用户不断询问某份大型文档),我们实际上是在为重复计算浪费金钱。更不用说延迟问题,没有人愿意在交互式应用中等待几十秒才能得到LLM的响应。
这就是为什么提示缓存(prompt caching)为什么出现,在使用Claude等LLM模型和Amazon Bedrock平台时,它可以显著降低成本并加快响应速度。
Prompt Caching
是一种通过存储和重用常用提示内容来提高LLM查询效率的技术。简单来说,它保存重复使用的prompt前缀(如系统指令或参考文档),这样模型就不必在后续请求中重新处理它们。
可以这样理解:通常当我们向LLM发送提示时,模型会从头到尾逐个token地阅读整个文本,构建其对上下文的内部表示。如果我们多次发送相同的内容,模型每次都会重复这项工作,不必要地消耗计算资源,产生很多费用。
通过提示缓存,我们可以告诉模型"记住这部分,我们稍后会重用它”,在后续请求中,它可以直接跳到处理新内容。
Amazon Bedrock的提示缓存实现,允许我们在提示中指定特定点作为"缓存检查点”。一旦设置了检查点,它之前的所有内容都会被缓存,并可以在后续请求中检索而无需重新处理。
但模型究竟是如何"记住"这些内容的呢?实际机制涉及保存模型的内部状态,即表示已处理token的注意力模式和隐藏状态向量,这样它们可以在后续请求中被加载而不是重新计算。
Prompt Caching
的好处是显著的,直接解决了前面提到的两个痛点:
降低延迟:通过避免重复处理相同的提示段,响应时间可以显著改善。Amazon Bedrock支持的模型缓存内容的响应速度最高可提升85%。
降低成本:从缓存中检索token而不是重新处理它们时,支付的费用显著降低,缓存token的成本通常只有常规输入token价格的约10%。这意味着缓存部分提示可能减少90%的成本。对于重复使用大块上下文的应用(例如有100页手册的文档问答),这将大量节省费用。
Amazon Bedrock 提示缓存支持 Claude 3.7 Sonnet、Claude 3.5 Haiku、Amazon Nova Micro、Amazon Nova Lite 和 Amazon Nova Pro - 2025-04-16
让我们深入了解Prompt Caching
在Amazon Bedrock中的实际工作机制:
当启用提示缓存时,Bedrock会在提示中的特定点创建"缓存检查点”。检查点标记了整个前面的提示前缀(到该点为止的所有token)被保存在缓存中的位置。在后续请求中,如果你的提示重用了相同的前缀,模型会加载缓存状态而不是重新计算它。
从技术角度来看,这个过程相当有趣。模型不只是存储原始文本,它保存了处理缓存部分后的整个内部状态。像Claude这样的现代LLM使用具有数百层注意力机制的Transformer架构, 在处理文本时,每一层都会生成表示文本中语义内容和关系的激活模式和状态向量。这些层通常需要为每个提示从头开始按顺序计算。
通过缓存,当我们发送包含相同缓存前缀的后续请求时,它不会重新运行所有这些计算,而是加载保存的状态并从那里继续。这就像给模型一个关于其自身思考过程的记忆,这样它可以回到一个思考一半的想法并重新思考之后的部分,而不是从头开始重新思考。
下面的图很形象的展示了这个过程:
缓存检查点具有最小和最大令牌数,取决于我们使用的特定模型。只有当我们的总提示前缀满足最小令牌数时,才能创建缓存检查点。例如,Anthropic Claude 3.7 Sonnet 模型每个缓存检查点至少需要 1,024 个令牌。
缓存有五分钟的生存时间 (TTL),每次成功的缓存命中都会重置。在此期间,缓存中的上下文会被保留。如果在 TTL 窗口内没有缓存命中,缓存将过期。
目前没有办法将这个TTL延长超过5分钟。为什么aws设置了这么短的时间呢?因为缓存状态存储在GPU内存中,而不是相对便宜的磁盘上。
上面的介绍如果第一次看,会有点抽象。结合实例来看容易理解
想象一下我们正在构建一个文档问答系统,用户可以上传文档然后提出多个问题。
如果不使用缓存,我们需要在每个新问题中发送整个文档,每次都会产生全额费用。
使用缓存,我们可以发送一次并重用它。
以下是如何使用Bedrock Converse API实现这一点:
import boto3
import base64
bedrock = boto3.client("bedrock-runtime")
document_text = """In Amazon EKS, each Pod that runs on a Windows host is assigned a secondary IP address by the VPC resource controller by default. This IP address is a VPC-routable address that is allocated from the host’s subnet. On Linux, each ENI attached to the instance has multiple slots that can be populated by a secondary IP address or a /28 CIDR (a prefix). Windows hosts, however, only support a single ENI and its available slots. Using only secondary IP addresses can artifically limit the number of pods you can run on a Windows host, even when there is an abundance of IP addresses available for assignment.
In order to increase the pod density on Windows hosts, especially when using smaller instance types, you can enable Prefix Delegation for Windows nodes. When prefix delegation is enabled, /28 IPv4 prefixes are assigned to ENI slots rather than secondary IP addresses. Prefix delegation can be enabled by adding the enable-windows-prefix-delegation: "true" entry to the amazon-vpc-cni config map. This is the same config map where you need to set enable-windows-ipam: "true" entry for enabling Windows support.
Please follow the instructions mentioned in the EKS user guide to enable Prefix Delegation mode for Windows nodes.
illustration of two worker subnets
Figure: Comparison of Secondary IP mode with Prefix Delegation mode
The maximum number of IP addresses you can assign to a network interface depends on the instance type and its size. Each prefix assigned to a network interface consumes an available slot. For example, a c5.large instance has a limit of 10 slots per network interface. The first slot on a network interface is always consumed by the interface’s primary IP address, leaving you with 9 slots for prefixes and/or secondary IP addresses. If these slots are assigned prefixes, the node can support (9 * 16) 144 IP address whereas if they’re assigned secondary IP addresses it can only support 9 IP addresses. See the documentation on IP addresses per network interface per instance type and assigning prefixes to network interfaces for further information.
During worker node initialization, the VPC Resource Controller assigns one or more prefixes to the primary ENI for faster pod startup by maintaining a warm pool of the IP addresses. The number of prefixes to be held in warm pool can be controlled by setting the following configuration parameters in amazon-vpc-cni config map.
warm-prefix-target, the number of prefixes to be allocated in excess of current need.
warm-ip-target, the number of IP addresses to be allocated in excess of current need.
minimum-ip-target, the minimum number of IP addresses to be available at any time.
warm-ip-target and/or minimum-ip-target if set will override warm-prefix-target.
As more Pods are scheduled on the node, additional prefixes will be requested for the existing ENI. When a Pod is scheduled on the node, VPC Resource Controller would first try to assign an IPv4 address from the existing prefixes on the node. If that is not possible, then a new IPv4 prefix will be requested as long as the subnet has the required capacity.
flow chart of procedure for assigning IP to pod
Figure: Workflow during assignment of IPv4 address to the Pod
Recommendations
Use Prefix Delegation when
Use prefix delegation if you are experiencing Pod density issues on the worker nodes. To avoid errors, we recommend examining the subnets for contiguous block of addresses for /28 prefix before migrating to prefix mode. Please refer " Use Subnet Reservations to Avoid Subnet Fragmentation (IPv4) " section for Subnet reservation details.
By default, the max-pods on Windows nodes is set to 110. For the vast majority of instance types, this should be sufficient. If you want to increase or decrease this limit, then add the following to the bootstrap command in your user data
-KubeletExtraArgs '--max-pods=example-value'
For more details about the bootstrap configuration parameters for Windows nodes, please visit the documentation here.
Avoid Prefix Delegation when
If your subnet is very fragmented and has insufficient available IP addresses to create /28 prefixes, avoid using prefix mode. The prefix attachment may fail if the subnet from which the prefix is produced is fragmented (a heavily used subnet with scattered secondary IP addresses). This problem may be avoided by creating a new subnet and reserving a prefix.
Configure parameters for prefix delegation to conserve IPv4 addresses
warm-prefix-target, warm-ip-target, and minimum-ip-target can be used to fine tune the behaviour of pre-scaling and dynamic scaling with prefixes. By default, the following values are used:
warm-ip-target: "1"
minimum-ip-target: "3"
By fine tuning these configuration parameters, you can achieve an optimal balance of conserving the IP addresses and ensuring decreased Pod latency due to assignment of IP address. For more information about these configuration parameters, visit the documentation here.
Use Subnet Reservations to Avoid Subnet Fragmentation (IPv4)
When EC2 allocates a /28 IPv4 prefix to an ENI, it has to be a contiguous block of IP addresses from your subnet. If the subnet that the prefix is generated from is fragmented (a highly used subnet with scattered secondary IP addresses), the prefix attachment may fail, and you will see the following node event:
InsufficientCidrBlocks: The specified subnet does not have enough free cidr blocks to satisfy the request
To avoid fragmentation and have sufficient contiguous space for creating prefixes, use VPC Subnet CIDR reservations to reserve IP space within a subnet for exclusive use by prefixes. Once you create a reservation, the IP addresses from the reserved blocks will not be assigned to other resources. That way, VPC Resource Controller will be able to get available prefixes during the assignment call to the node ENI.
It is recommended to create a new subnet, reserve space for prefixes, and enable prefix assignment for worker nodes running in that subnet. If the new subnet is dedicated only to Pods running in your EKS cluster with prefix delegation enabled, then you can skip the prefix reservation step.
Replace all nodes when migrating from Secondary IP mode to Prefix Delegation mode or vice versa
It is highly recommended that you create new node groups to increase the number of available IP addresses rather than doing rolling replacement of existing worker nodes.
When using self-managed node groups, the steps for transition would be:
Increase the capacity in your cluster such that the new nodes would be able to accomodate your workloads
Enable/Disable the Prefix Delegation feature for Windows
Cordon and drain all the existing nodes to safely evict all of your existing Pods. To prevent service disruptions, we suggest implementing Pod Disruption Budgets on your production clusters for critical workloads.
After you confirm the Pods are running, you can delete the old nodes and node groups. Pods on new nodes will be assigned an IPv4 address from a prefix assigned to the node ENI
When using managed node groups, the steps for transition would be:
Enable/Disable the Prefix Delegation feature for Windows
Update the node group using the steps mentioned here. This performs similar steps as above but are managed by EKS
Warning
Run all Pods on a node in the same mode
For Windows, we recommend that you avoid running Pods in both secondary IP mode and prefix delegation mode at the same time. Such a situation can arise when you migrate from secondary IP mode to prefix delegation mode or vice versa with running Windows workloads.
While this will not impact your running Pods, there can be inconsistency with respect to the node’s IP address capacity. For example, consider that a t3.xlarge node which has 14 slots for secondary IPv4 addresses. If you are running 10 Pods, then 10 slots on the ENI will be consumed by secondary IP addresses. After you enable prefix delegation the capacity advertised to the kube-api server would be (14 slots * 16 ip addresses per prefix) 244 but the actual capacity at that moment would be (4 remaining slots * 16 addresses per prefix) 64. This inconsistency between the amount of capacity advertised and the actual amount of capacity (remaining slots) can cause issues if you run more Pods than there are IP addresses available for assignment.
That being said, you can use the migration strategy as described above to safely transition your Pods from secondary IP address to addresses obtained from prefixes. When toggling between the modes, the Pods will continue running normally and:
When toggling from secondary IP mode to prefix delegation mode, the secondary IP addresses assigned to the running pods will not be released. Prefixes will be assigned to the free slots. Once a pod is terminated, the secondary IP and slot it was using will be released.
When toggling from prefix delegation mode to secondary IP mode, a prefix will be released when all the IPs within its range are no longer allocated to pods. If any IP from the prefix is assigned to a pod then that prefix will be kept until the pods are terminated.
Debugging Issues with Prefix Delegation
You can use our debugging guide here to deep dive into the issue you are facing with prefix delegation on Windows."""
# 第一次交互 - 缓存文档
messages = [{"role": "user", "content": []}]
# 将文档添加到用户消息
messages[0]["content"].append({"text": document_text})
# 在文档后标记缓存检查点
messages[0]["content"].append({"cachePoint": {"type": "default"}})
# 添加用户的第一个问题
messages[0]["content"].append({"text": "这个文档中的主要主题是什么?"})
# print(f"第一次请求 messages: {messages}")
try:
response = bedrock.converse(
modelId="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
messages=messages
)
print(response)
# 通过查看使用指标检查缓存是否工作
usage = response['usage']
print(f"第一次请求usage: {usage}")
except Exception as e:
print(f"第一次请求出错: {e}")
# 如果缓存失败,实施回退策略
# 第二次交互 - 重用缓存文档
followup_question = "你能详细说明第二点吗?"
messages = [
{"role": "user", "content": [{"text": document_text}, {"cachePoint": {"type": "default"}},]},
{"role": "assistant", "content": [{"text": response["output"]['message']["content"][0]["text"]}]},
{"role": "user", "content": [{"text": followup_question}]}
]
try:
response2 = bedrock.converse(
modelId="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
messages=messages
)
# 验证缓存命中
usage = response2['usage']
print(f"第二次请求usage: {usage}")
except Exception as e:
print(f"第二次请求出错: {e}")
# 适当处理错误
输出结果如下:
第一次请求usage: {'inputTokens': 18, 'outputTokens': 101, 'totalTokens': 2154, 'cacheReadInputTokens': 0, 'cacheWriteInputTokens': 2035}
第二次请求usage: {'inputTokens': 121, 'outputTokens': 446, 'totalTokens': 2602, 'cacheReadInputTokens': 2035, 'cacheWriteInputTokens': 0}
在这个例子中,第一次调用converse
处理并缓存文档。对于第二个问题,我们在对话历史中再次包含文档,但因为它已被缓存,Bedrock将从缓存中检索它而不是重新处理它。这显著加快了响应速度并降低了成本。
以下是成本结构的工作方式:
让我们通过一个例子来看看实际数字的成本影响:
假设有一个20000个token的文档,用户经常询问相关问题。如果不使用缓存,当用户在一个会话中提出10个问题时,需要处理该文档10次,总共200000个输入token。
使用提示缓存:
让我们用Claude 3.5 Sonnet的定价来分解计算:
输入token:
输出token:每1K token $0.015(使用缓存时保持不变)
不使用缓存:
使用缓存:
总节省:$0.60 - $0.129 = $0.471,约78%
但是,让我们扩展到更现实的场景。假设你有一个企业文档问答系统,每天处理1,000个针对10个不同文档(每个20,000个token)的查询。假设每个文档有100个查询:
不使用缓存:
使用缓存:
月节省:$1800 - $200.70 = $1599.30,约89%
这仅仅通过实施提示缓存就能每年节省超过$20000
对于处理更大文档或在5分钟窗口内更频繁重用的应用,计算结果会更加引人注目。对于处理许多类似请求的应用,节省可能每月达到数千美元。
值得注意的是,即使有缓存写入的开销,只要你至少重用一次缓存内容,提示缓存几乎总是更具成本效益。收支平衡点非常低,使这成为大多数LLM应用的显而易见的优化。
在对话式AI中,我们通常提供一个固定的"system"提示,定义助手的角色、能力和行为指南。如果不使用缓存,模型会在每个用户轮次处理相同的指令块,每次都增加延迟和成本。
通过Bedrock的提示缓存,我们可以在第一轮后缓存这些指令。模型将在后续用户消息中重用缓存的指令,有效地"记住"其"个性"而无需从头重新阅读。
这对于在对话中维持状态的助手特别有价值,比如可以访问账户详情或订单历史的客户服务机器人。我们可以一次缓存这种用户特定的上下文,并快速、低成本地处理多轮问答。
例如,如果我们正在构建一个帮助用户规划旅行的旅行助手,可能有:
如果不使用缓存,每条用户消息都需要重新处理所有6000个token。使用缓存,后续消息只需处理新的用户输入,每轮可能节省数千个token。在一个典型的有10-20轮的对话中,这些节省很快就会累积起来。
AI编码助手通常需要在其提示中包含相关的代码上下文。这可能是项目摘要、某些文件的内容或正在编辑的最后N行代码。
提示缓存可以通过保持上下文随时可用来优化AI驱动的编码助手。编码助手可能会一次缓存大段代码片段,然后重用它来回答关于该代码的多个查询。
例如,如果开发者上传一个模块并询问"这个函数是如何工作的?",然后是"我如何优化这个循环?",助手可以从缓存中提取代码的表示而不是重新处理它。从而有更快的建议和更低的token使用量。
最引人注目的使用场景之一是涉及模型必须分析或回答问题的大型文档或文本的任何场景。例如:
这些通常涉及将非常长的文本(数万个token)输入到模型中。提示缓存一次将整个文档嵌入到提示中,缓存它,然后提出多个问题而无需每次都重新处理完整文本。
一个典型的合同可能有30-50页,或大约15000-25000个token。如果不使用缓存,关于合同的每个问题都需要再次发送所有这些token。使用缓存,第一个问题可能需要几秒钟处理,但后续问题可能在不到一秒的时间内返回答案,成本只是一小部分。
虽然提示缓存提供了显著的好处, 但也有限制:
5分钟TTL约束: 缓存内容的5分钟生存时间是一个硬性限制。如果应用在相关请求之间有更长的不活动期,缓存将过期,需要重新处理完整的提示。这对于用户可能会休息或分心的应用来说特别具有挑战性。为了缓解这一点,我们可以实现一个后台进程,通过虚拟请求定期"刷新"重要的缓存。
最小token阈值: 如前所述,缓存检查点只能在满足特定于模型的最小token计数后设置。如果提示短于此阈值,缓存将不起作用。这意味着缓存对于非常短的提示好处较小。
相同前缀要求: 缓存只有在重用完全相同的前缀时才有效。即使对缓存内容进行微小的更改或编辑也会导致缓存未命中。这对于需要对其他稳定的上下文进行小更新的应用来说可能是个问题。
调试复杂性: 当提示缓存不按预期工作时,调试可能具有挑战性。缓存机制在很大程度上是不透明的,只能通过响应中的使用指标来确定缓存是否正常工作。这可能使生产问题的故障排除更加困难。
跨会话限制: 缓存不在不同会话或用户之间共享。每个对话或会话都有自己独立的缓存,所以即使用户访问相同的内容,你也不能从跨不同用户的缓存中受益。
尽管有这些限制,对于大多数用例,提示缓存的好处通常超过缺点。
现在我们已经涵盖了提示缓存的内容、原因和方式,让我们谈谈一些最佳实践,以充分利用它:
考虑缓存结构化提示: 将静态内容(如指令、参考材料或文档)放在提示的早期,然后是缓存检查点,然后是动态内容(如特定问题)。这使静态部分可缓存。例如,在文档问答系统中,首先放置文档文本,然后是缓存检查点,然后是用户的问题。
注意模型的最小token数: 记住不同的模型对检查点有不同的最小token要求,所以相应地设计。
监控缓存使用: 检查缓存读/写指标以验证缓存检查点是否按预期工作。如果你在缓存读取计数器中没有看到token,缓存可能已过期或未正确设置。在应用中添加日志记录以随时间跟踪这些指标,并对意外模式发出警报。
记住5分钟TTL: 如果用户交互可能暂停超过5分钟,需要重新发送上下文以重新准备缓存。考虑构建一个机制来检测何时需要缓存刷新,也许通过跟踪最后一次缓存命中的时间戳,如果接近TTL限制,则主动刷新。
测试不同的缓存位置: 尝试放置缓存检查点的位置,并监控对token使用和延迟的影响。最佳配置可能因你的特定用例而异。A/B测试不同的缓存策略可以帮助确定最有效的方法。
适当使用多个检查点: 对于具有多个不同部分的复杂提示,考虑使用多个缓存检查点。这对于随时间演变的对话特别有用,允许你独立缓存不同的段。例如,在文档问答系统中,你可能在文档之后有一个检查点,在摘要或分析之后有另一个检查点。这也允许你轻松实现对话分支:从对话中的特定点分支出多个对话的能力。你可以在检查点中保存每个分支点,而不需要重新处理分支前整个对话的上下文。
考虑特定模型的限制: 不同的模型支持不同数量的缓存检查点,并且有不同的最小token要求。根据你的特定模型的约束设计你的缓存策略。例如,如果你使用的模型只支持单个检查点,优先缓存你提示中最大或最昂贵的部分。
通过遵循这些实践,可以最大化提示缓存的成本节省和性能优势,同时避免常见陷阱。