Groundhog Day for AI agents: Prompt caching patterns for using OpenAI models on Amazon Bedrock

TL;DR · AI 摘要
Groundhog Day for AI agents: Prompt caching patterns for using OpenAI models on Amazon Bedrock Artificial Intelligence G...
核心要点
- 主题聚焦:Groundhog Day for AI agents: Prompt caching patt
- 来源:AWS Machine Learning Blog,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
AI代理的“土拨鼠日”:在Amazon Bedrock上使用OpenAI模型的提示缓存模式 | 人工智能
AI代理的“土拨鼠日”:在Amazon Bedrock上使用OpenAI模型的提示缓存模式
每次启动代理时,它都必须重新阅读自己的全部历史记录才能执行操作。以下是如何让它记住信息而无需重复提供相同上下文的方法。
在电影《土拨鼠日》中,比尔·默里饰演的角色每天重复经历同一天的2月2日。他每天早上都会在同一家旅馆、同一个小镇中醒来,收音机里播放着相同的歌曲,这种重复据一些影迷估计持续了34年。
除非他重新从头开始推导,否则前一天取得的任何成就都不会延续。AI代理也生活在类似“土拨鼠日”的状态中。每当代理执行一个操作(调用工具、读取结果、决定下一步行动)时,都必须将迄今为止的完整故事发送给模型以保持连贯性:工具定义、指令、所有之前的工具调用和结果。模型仅根据您提供的输入预测下一个标记,请求之间没有内部记忆,因此必须重新阅读所有内容。在编码会话的第20轮时,模型会重新读取前19轮的内容,而您需要为每一轮的每个标记重复支付费用。
提示缓存打破了这种循环。该服务会存储您的输入提示(指令、工具和内容),并在后续请求中重复使用,费用仅为原始输入价格的一小部分。
在本文中,我将带您了解代理从首次交互到获得新工具并生成助手的完整生命周期。在此过程中,我将展示提示缓存如何降低成本并保持代理的响应性和一致性。
代理发送给LLM的内容
在讨论缓存之前,先了解典型代理首次调用大语言模型(LLM)时包含的内容会有所帮助:
- 工具列表 – 代理可以调用的每个函数的定义。当您发布新版本时会发生变化。
- 指令 – 开发者提示:规则、角色设定、政策、示例。每个版本都会变化。
- 历史记录 – 会话到目前为止发生的一切:任务、模型的工具调用和工具结果(每轮都会增长)。
- 新轮次 – 最新的工具结果或用户消息(每次可能不同)。
如果没有设置缓存,代理在每一轮都会向模型发送以下内容:
图1:代理在每一轮向模型发送的内容
提示缓存的底层工作原理
当模型读取提示时,首先会执行一个称为预填充(prefill)的写入操作。在预填充阶段,模型会处理每个输入标记并构建其键和值向量(也称为键值对或KV),这些向量构成模型的工作状态。这发生在模型生成第一个标记之前。
提示缓存会保存这个状态,其中包含系统提示、工具、内容、历史记录等信息。当后续请求以相同的输入标记(即相同文本)开头时,提示缓存服务会加载已保存的状态,而不是重新计算。然后,它只处理发送给模型的下一个有效负载中的新内容。提示缓存可以是隐式的(Amazon Bedrock自动选择可重用前缀的结束位置),也可以是显式的(您的代码使用prompt_cache_breakpoint标记该端点)。您可以同时使用这两种方法,我们建议这样做。
第1轮与第2轮的对比
Figure 2: 第1轮保存提示前缀到断点位置,第2轮复用该前缀并仅对新问题支付完整费用
隐式缓存与显式缓存,为何需要两者兼用?
对于代理工作负载,隐式缓存使Bedrock能够根据对话增长自动选择缓存内容。显式缓存则允许您标记共享内容(如工具和指令),以便在会话之间及创建辅助代理时重复使用。您并非总是需要同时使用这两种缓存方式,但结合使用可提供可复用的起点,同时让Bedrock自动管理不断增长的对话。
新会话也可以复用该共享起点,而每个对话则基于自己的缓存历史进行构建。在本文中,我们首先从第一轮开始,然后展示如何在后续轮次、新工具和子代理之间传递缓存内容。
你好,缓存世界
首次突破是缓存提示的静态部分:跨请求保持不变的工具和指令。这些示例使用您现有的Amazon Bedrock客户端、TOOL和任务。请将缩短的INSTRUCTIONS字符串替换为实际的策略和示例。可复用的前缀必须达到1,024个token才能符合要求。
缓存静态内容时,请使用显式模式:选择保存的前缀结束位置。
KEY = "support-agent:v7:gpt-6-astra"
INSTRUCTIONS = "您的完整支持策略和示例..."
# 渲染上下文:[提供商上下文][工具][指令] | [任务]
# 缓存从开头开始;下方标记选择结束位置
resp = client.responses.create(
model="us.openai.gpt-6-astra",
store=False, # 响应存储;提示缓存仍有效
include=["reasoning.encrypted_content"],
prompt_cache_key=KEY,
prompt_cache_options={"mode": "explicit", "ttl": "30m"},
tools=TOOLS, # 在指令之前渲染;保持顺序稳定
input=[
{
"role": "developer",
"content": [{
"type": "input_text",
"text": INSTRUCTIONS,
# 保存整个块,包括工具
"prompt_cache_breakpoint": {"mode": "explicit"},
}],
},
# 用户消息被处理但不缓存
{"role": "user", "content": task},
],
)prompt_cache_key告诉服务哪些请求旨在共享缓存。我们将在后续会话中读取缓存时再讨论如何使用它。在我们使用显式缓存的测试中,约4,000个token的工具和指令前缀在首次请求时写入了4,120个token,并在后续请求中读取了这4,120个token。任务(或用户请求)位于缓存边界之后,因此不会触发缓存写入。
请注意,提示缓存仅在传递完全相同的文本时有效。请确保代理的指令和工具列表每次序列化方式一致。模型上下文协议(MCP)服务器和其他工具交付形式需要进行索引以确保一致性。将任何变化内容(如日期、用户名或请求ID)置于缓存指令之后。
随着内容的增长和变化(不同工具和内容),使用隐式模式让服务跟随对话。在此模式下,首次请求没有显式标记。其最新有效消息是用户的任务,因此自动断点会在该任务之后,覆盖工具、指令和任务内容。为清晰起见,以下示例显式展示了该模式,尽管隐式是默认设置。将 store=False 保留对话历史记录在您的应用中。它不会禁用缓存。
def ask_with_implicit_cache(history):
return client.responses.create(
model="us.openai.gpt-6-astra",
store=False,
include=["reasoning.encrypted_content"],
prompt_cache_key=KEY,
prompt_cache_options={"mode": "implicit", "ttl": "30m"},
tools=TOOLS,
input=history, # Everything gets cached
)
history = [
{"role": "developer", "content": INSTRUCTIONS},
{"role": "user", "content": task},
]
# 自动端点:用户消息的结尾
resp = ask_with_implicit_cache(history)当后续回合继续执行相同任务时,这种模式非常有用。不同任务不会通过旧任务匹配前缀,隐式写入不一定仅为工具和指令创建单独条目。当需要该较短共享前缀在任务间可重复使用时,请在该位置使用显式标记。
当代理需要共享起始点和增长历史记录时,两者都需要使用。从以下代码示例中标记的开发者代码块开始,并调用 ask_with_implicit_cache 函数。请求级模式保持隐式。块级标记在指令后添加显式保存点。服务也可以通过最新有效消息进行保存。这在对话增长时保持共享前缀可用。
history = [
{
"role": "developer",
"content": [{
"type": "input_text",
"text": INSTRUCTIONS,
"prompt_cache_breakpoint": {"mode": "explicit"},
}],
},
{"role": "user", "content": task},
]
# 仍为隐式模式,现在在历史记录中包含显式启动保存点
resp = ask_with_implicit_cache(history)缓存对话历史记录和工具结果
模型通过名称请求工具并提供其参数。您的应用必须执行这些操作。execute_function 是您构建的辅助函数,用于将这些请求路由到匹配的 Python 函数并返回结果。以下循环使用此辅助函数运行每个请求的工具。它将模型的输出和所有工具结果追加到历史记录列表中,然后再次发送该列表,直到模型返回最终答案。在隐式模式下,Amazon Bedrock 可以重用未更改的前缀,并通过最后一次工具结果缓存新增内容。保留早期消息和初始 TOOLS 的不变性。
import json
# 将工具名称映射到其 Python 实现
TOOL_HANDLERS = {
"get_owner": get_owner,
# 在此处注册其他初始工具
}
def execute_function(name, arguments):
"""运行请求的工具并返回结果"""
handler = TOOL_HANDLERS.get(name)
if handler is None:
return {"error": f"Unknown tool: {name}"}
return handler(**arguments)while True:
# 保留所有输出项,包括加密的推理内容
history.extend(resp.output)
calls = [x for x in resp.output if x.type == "function_call"]
if not calls:
break
for call in calls:
result = execute_function(call.name, json.loads(call.arguments))
history.append({
"type": "function_call_output",
"call_id": call.call_id,
"output": json.dumps(result),
})
# 该组的最后一个结果提供自动端点
resp = ask_with_implicit_cache(history)对于后续操作,追加新的用户消息并再次调用辅助函数。保持早期消息不变以确保其缓存端点仍然可用。
滚动断点:每轮读取所有先前内容
图3:滚动断点,每轮读取所有先前内容并仅写入自己的工具结果
对于单独的显式模式运行,从合并的"Hello cached world"示例中重新创建两消息历史记录。此循环将工具结果封装在input_text块中,并标记每批的最终结果。早期标记会保留在历史记录中。请参阅当前长对话的查找规则。
import json
while True:
resp = client.responses.create(
model="us.openai.gpt-6-astra", store=False,
include=["reasoning.encrypted_content"],
tools=TOOLS, prompt_cache_key=KEY, input=history,
prompt_cache_options={"mode": "explicit", "ttl": "30m"},
)
d = resp.usage.input_tokens_details
print("read", d.cached_tokens, "write", d.cache_write_tokens)
history.extend(resp.output) # 包含加密的推理内容
calls = [x for x in resp.output if x.type == "function_call"]
if not calls:
break
batch = []
for call in calls:
result = execute_function(call.name, json.loads(call.arguments))
batch.append({
"type": "function_call_output", "call_id": call.call_id,
"output": [{"type": "input_text", "text": json.dumps(result)}],
})
# 添加最新边界;保留所有早期标记
batch[-1]["output"][-1]["prompt_cache_breakpoint"] = {"mode": "explicit"}
history.extend(batch)GPT-6 Astra在六轮工作负载中与另一个模型并列获得最佳隐式结果:相比不使用缓存的运行方式,输入成本降低了约62%。显式滚动断点在相同模型上节省了约61%的成本。随着对话增长,两者都重用了更多历史记录。这些是工作负载结果。请测量您自己的代理节省情况。
图4:GPT-6 Astra上每种缓存设置的输入成本(相对于无缓存)
需要注意:
- 仅显式模式下的静态断点 – 工具和指令被缓存,但每次轮次都会重新处理增长的历史记录(0.61)。此限制不适用于合并示例,后者还能通过符合条件的对话结束实现节省。
- 在显式模式下移动单个断点可能阻止缓存重用并导致重复写入。请保留前一轮的断点。
- 仅通过变化任务的隐式写入 – 新任务可能共享工具和指令但不共享先前保存的端点。如果不同任务需要重用较短前缀,请在共享指令后添加显式标记。
当工具发生变化时
有时代理的工具列表会发生变化,或者新增工具。保持初始列表稳定:添加、删除或重新排序定义会使缓存的前缀失效。选择适合工具可用方式的方法。
当已知工具在代理流程的不同步骤中被调用时:发送相同的排序后的TOOLS,并使用allowed_tools限制模型可调用的范围。通过allowed_tools,"mode": "auto"允许模型决定是否调用允许的工具,而"mode": "required"要求至少调用一个允许的工具。将此作为tool_choice传递。该限制不会改变缓存的目录:
tool_choice={"type": "allowed_tools", "mode": "auto",
"tools": [{"type": "function", "name": "get_owner"}]}会话期间的新工具。Amazon Bedrock Runtime和Amazon Bedrock Mantle端点均接受一种名为additional_tools的输入项类型,用于GPT-6 Astra、GPT-6.1 Sol和GPT-5.6 Luna。有关AWS区域的模型可用性,请参阅Amazon Bedrock中的“按AWS区域支持的模型”。在辅助函数execute_function中注册新工具get_due_date,然后将其模式追加到代理循环生成的历史记录中。保持初始TOOLS不变:
TOOL_HANDLERS["get_due_date"] = get_due_date
NEW_TOOL = {
"type": "function", "name": "get_due_date",
"description": "返回支持案例的截止日期。",
"strict": True,
"parameters": {
"type": "object",
"properties": {"case_id": {"type": "string"}},
"required": ["case_id"], "additionalProperties": False,
},
}
history.append({
"type": "additional_tools", "role": "developer",
"tools": [NEW_TOOL],
})
history.append({
"role": "user", "content": "查找案例C-123的截止日期。",
})
# 隐式模式通过这个新用户消息保存,包括NEW_TOOL。
resp = ask_with_implicit_cache(history)
d = resp.usage.input_tokens_details
print("旧前缀读取", d.cached_tokens, "新写入", d.cache_write_tokens)
# 示例输出:
# 旧前缀读取 2048 新写入 256会话中途到达新工具
图5:附加工具保持早期前缀可重复使用,示例数据来自Amazon Bedrock Runtime上的GPT-6 Astra
使用新响应继续工具执行循环。循环将模型的输出添加到历史记录中,运行任何请求的工具,并追加其结果。在后续请求中保留additional_tools项,使新工具保持可用。在隐式模式下,Amazon Bedrock可通过新用户消息进行缓存,包括其前面的工具定义。在仅显式模式下,保留早期断点,并在该用户消息的input_text块中添加一个断点。additional_tools项本身无法保存断点。
不同角色,不同目录。当不同代理角色需要不同工具时,在每次会话开始时选择一个稳定的工具配置文件,例如支持或计费。每个配置文件将获得自己的缓存前缀。标记共享指令,以便后续使用该配置文件的任务可以重复使用它们,然后随着对话发展使用任一模式。
缓存的工具配置文件
图6:每个工具配置文件都有自己的缓存前缀,可被匹配会话重复使用
子代理和旁路调用
委托处理不应让每个辅助程序都重复阅读相同的背景信息。共享前缀,然后在末尾添加每个辅助程序的任务。请注意,当使用缓存与AI代理时,工具和系统提示可能会被泄露或被利用。建立负责任的AI实践,将您的提示缓存实现作为治理的一部分。
先预热,然后分发任务。首先发送一个普通请求并等待其完成后再启动辅助程序。在每个请求中明确标记共享上下文,即使模式是隐式的:
shared = {"role": "developer", "content": [{
"type": "input_text", "text": INSTRUCTIONS,
"prompt_cache_breakpoint": {"mode": "explicit"},
}]}
warm = client.responses.create(
model="us.openai.gpt-6-astra", store=False,
tools=TOOLS, prompt_cache_key=KEY,
prompt_cache_options={"mode": "implicit", "ttl": "30m"},
max_output_tokens=16,
input=[shared, {"role": "user", "content": "Reply READY."}],
)
d = warm.usage.input_tokens_details
if not (d.cache_write_tokens or d.cached_tokens):
raise RuntimeError("Shared prefix was not cached.")
from concurrent.futures import ThreadPoolExecutor
# helper_tasks 包含每个辅助程序的任务文本。
helper_inputs = [
[shared, {"role": "user", "content": task}]
for task in helper_tasks
]
with ThreadPoolExecutor(max_workers=4) as pool:
helpers = list(pool.map(ask_with_implicit_cache, helper_inputs))辅助程序使用与预热相同的 TOOLS、模型和 KEY。即使预热在输出限制处结束,仍然可以填充缓存。使用检查确认缓存活动在辅助程序启动之前。
子代理:预热共享前缀一次,然后分发任务
图7:在启动辅助程序之前预热共享前缀,每个辅助程序随后添加自己的任务
侧边调用使用相同的开头。代理循环通过追加每个 resp.output 及其对应的工具结果(包括加密推理)来构建历史记录。父代理完成后,将该完整列表复制给摘要器或判断器。相同的辅助程序保持工具、模型和密钥不变:
from copy import deepcopy
# history 是上面代理循环构建的完整历史记录。
# 复制它以便辅助程序可以添加指令而不会修改父代理。
helper_history = deepcopy(history)
helper_history.append({
"role": "developer",
"content": "用一句话总结这次对话。不要调用工具。",
})
summary = ask_with_implicit_cache(helper_history)
d = summary.usage.input_tokens_details
print("history read", d.cached_tokens, "write", d.cache_write_tokens)
print(summary.output_text)
# 示例输出(假设值和对话):
# history read 4096 write 32
# 案例 C-123 于 2026 年 10 月 31 日到期。辅助程序可以重用历史记录中缓存的部分,但追加的开发者消息不会自动扩展缓存以包含摘要指令。如果后续请求重用相同的历史记录和指令,请将指令放在带有 prompt_cache_breakpoint 的 input_text 块中。对于一次性摘要,不需要额外标记。
暂停、重启和缓存密钥
暂停。缓存前缀的最小生存时间(TTL)为30分钟,通过成功重用刷新,并可能持续更长时间。如果会话很快恢复,普通预热可以在缓存过期前刷新前缀。将该请求的成本与让缓存过期进行比较。有关TTL规则,请参阅Amazon Bedrock文档。
新会话。对于旨在共享前缀的请求,保持 prompt_cache_key 稳定。会话特定的键即使提示匹配也能隔离会话。在以下示例中,切换到共享键使会话能够重用前缀。
新会话仅在共享键时重用缓存
图8:会话特定键分离缓存条目,共享键允许匹配会话重用它们
现有缓存内容的重用需要相同的模型、AWS 账户和区域,并且需要有可用的匹配前缀。以下示例结合了显式启动标记和隐式模式。每个会话都以相同指令开始并拥有自己的任务。两者使用相同的计算键:
import hashlib
def cache_key(app, model, prompt_version, instructions):
digest = hashlib.sha256(instructions.encode()).hexdigest()[:12]
return f"{app}:{prompt_version}:{model}:{digest}"
KEY = cache_key("support-agent", "us.openai.gpt-6-astra", "v7", INSTRUCTIONS)
prefix = {"role": "developer", "content": [{
"type": "input_text", "text": INSTRUCTIONS,
"prompt_cache_breakpoint": {"mode": "explicit"},
}]}
for label, question in [
("session A", "Explain the refund policy without calling tools."),
("session B", "Explain the escalation policy without calling tools."),
]:
# 每个会话的新对话;启动前缀匹配。
session_history = [prefix, {"role": "user", "content": question}]
result = ask_with_implicit_cache(session_history)
d = result.usage.input_tokens_details
print(label, "read", d.cached_tokens, "write", d.cache_write_tokens)冷启动时,会话1写入共享前缀。即使任务不同,会话2仍可读取它。每个任务也可以隐式缓存。检查使用计数器:匹配的键和标记使重用成为可能,但缓存条目仍需可用。
提示缓存能实现什么
成本。对于使用8,000个标记启动前缀且每次交互增加2,000个标记的GPT-6 Astra代理,一次性静态缓存将成本降至0.50美元,滚动断点使模型输入成本降低64%:从每会话0.87美元降至0.32美元。
图9:使用Amazon Bedrock按需价格计算的GPT-6 Astra支持代理示例的每会话累计输入成本
如何判断缓存是否生效
从响应使用情况开始。cached_tokens报告从缓存读取的输入量,cache_write_tokens报告写入缓存的输入量。随着对话增长,检查后续回合是否重用早期历史,并确保新写入反映新增内容。
在Amazon Bedrock Runtime端点进行生产监控时,使用Amazon CloudWatch中的CacheReadInputTokenCount和CacheWriteInputTokenCount指标。需要单个请求记录时启用模型调用日志,然后通过Amazon CloudWatch Logs Insights查询它们。对于Amazon Bedrock Mantle端点,需在应用程序日志中记录响应使用字段。其Amazon CloudWatch命名空间目前没有单独的缓存计数器。
要检查成本,请在AWS Cost Explorer中筛选Amazon Bedrock并按使用类型分组。缓存读取、缓存写入和未缓存输入会作为每个模型的独立条目显示。计算它们对输入成本的占比时,请排除输出标记条目。
简短的检查清单
- 保持可复用前缀的稳定性。优先放置共享工具和指令,随后追加对话历史和新对话轮次。修改时间戳、消息或工具定义会阻止其在变更后继续复用。如果前缀本身有独立的保存端点,较短的前缀仍可被复用。
- 在希望复用的内容周围设定边界。对增长型历史使用隐式缓存,对共享前缀使用显式标记,或两者结合使用。仅使用显式模式可避免生成临时后缀。在扩展仅显式模式的对话时,需保留早期标记,以便后续请求能定位到已保存的前缀。
- 保持初始工具目录不变。使用allowed_tools限制可调用工具范围。通过additional_tools追加已知定义,或通过命名空间客户端工具搜索(namespaced client-side tool_search)进行发现。当不同代理角色需要不同工具目录时,选择稳定的工具配置文件。
- 检查前缀大小和生命周期。可缓存前缀必须包含至少1,024个token。缓存前缀在其最后一次写入或成功复用后至少30分钟内保持复用资格。
- 在启动辅助服务前预热共享内容。在预热请求和辅助请求中都标记共享前缀,包括使用隐式模式时。在启动并行调用前必须完成预热请求。
- 在每个共享组内保持缓存键稳定。旨在复用前缀的请求必须使用相同键。使用不同键可在客户或用户之间保持缓存计费独立性。
- 跨工作流衡量节省效果。缓存写入成本是正常输入速率的1.25倍。读取价格取决于模型。在开发阶段使用响应使用情况,在生产环境使用监控和计费数据,同时跟踪写入成本和读取节省效果。
2月3日的觉醒
比尔·默里只有在停止重复相同错误并开始将每天学到的内容带入第二天时,才能摆脱《土拨鼠之日》的循环。代理以类似但更现实的方式达成这一点:通过逐字节保持提示开头的稳定性,并随着历史增长使用自动或显式缓存边界。
在本文中,我们介绍了如何缓存共享工具和指令,跨代理轮次扩展缓存,在添加工具、启动辅助服务或开始新会话时保持复用性。随着工作流推进,保持共享前缀稳定并追加新信息。对增长型对话使用隐式缓存,对共享内容使用显式断点。需要两者时结合使用,当变更后缀不太可能被复用时选择仅显式模式。通过比较缓存读取、写入、总输入成本和延迟,找到最适合您代理的方案。要应用这些模式,请遵循OpenAI模型的Amazon Bedrock提示缓存文档,并通过AWS区域和端点检查模型可用性。从一个工作流开始,随着对话增长衡量节省效果。
关于作者
'"` /think