AI 智能体的“土拨鼠之日”:在 Amazon Bedrock 上使用 OpenAI 模型的提示词缓存模式
Groundhog Day for AI agents: Prompt caching patterns for using OpenAI models on Amazon Bedrock
在 Amazon Bedrock 上调用 OpenAI 模型时,提示词缓存可让智能体复用已保存的 KV 状态,避免每轮重复处理工具定义、指令和历史。测试中约 4,000 token 的工具与指令前缀首次写入 4,120 token,后续请求直接读取同样 4,120 token。
每次你启动智能体,它都必须重新阅读自己的完整经历才能执行操作。下面介绍如何让它记住,而不必反复向它提供相同的上下文。
在电影《土拨鼠之日》中,比尔·默里饰演的角色每天都会重新经历同一个2月2日。他在同一家旅店、同一个小镇醒来,收音机里播放着同一首歌,如此反复,一些影迷估计长达34年。
除非他从头重新完成,否则前一天取得的任何成果都不会延续。AI智能体也生活在某种《土拨鼠之日》里。智能体每执行一步(调用工具、读取结果、决定下一步做什么),都必须把到目前为止的完整经历发送给模型以保持连续性:工具定义、指令、此前每一次工具调用以及每一个结果。模型只根据你提供的输入预测下一个token。它在请求之间没有内部记忆,因此必须把全部内容重新读一遍。到编码会话的第20轮时,模型会重新阅读第1至19轮,而你要为这些token一次又一次地付费。
提示缓存打破了这一循环。该服务会存储你的输入提示(指令、工具和内容),并在下一次请求时复用,价格仅为输入价格的一小部分。
在本文中,我将带你了解智能体的一生,从它的第一轮开始,到它获得新工具并生成辅助智能体。在此过程中,我会展示提示缓存如何降低成本,并让智能体保持响应迅速和一致。
智能体向LLM发送什么
在讨论缓存之前,先看看典型智能体首次调用大语言模型(LLM)时包含哪些内容会有所帮助:
- 工具列表 – 智能体可调用的每个函数的定义。发布新版本时会变化。
- 指令 – 开发者提示:规则、角色设定、策略、示例。同样会随版本变化。
- 历史记录 – 本次会话到目前为止发生的一切:任务、模型的工具调用以及工具结果(每轮都会增长)。
- 新一轮 – 最新的工具结果或用户消息(每次可能不同)。
如果不设置缓存,智能体每轮向模型发送的内容:
图1:智能体每轮向模型发送的内容
提示缓存的底层工作原理
当模型读取提示时,它首先执行一次称为预填充的写入。在预填充期间,它处理每个输入token并构建键值向量(也称为键值,或KV),这些向量构成模型的工作状态。这发生在模型写出第一个token之前。
提示缓存会保存该状态,其中包含系统提示、工具、内容、历史记录等。当后续请求以相同的输入token(即相同的文本)开头时,提示缓存服务会加载已保存的状态,而不是重新计算。然后它只处理下一次发送给模型的载荷中的新内容。提示缓存可以是隐式的,由Amazon Bedrock选择可复用前缀的结束位置;也可以是显式的,由你的代码使用prompt_cache_breakpoint标记该端点。你可以同时使用这两种方式,我们也建议这样做。
第1轮与第2轮对比
图2:第1轮保存到断点为止的提示前缀,第2轮复用它,只为新问题支付全价
隐式还是显式,为什么两者都需要?
对于智能体工作负载,隐式缓存让 Bedrock 随着对话增长自行选择要缓存的内容。使用显式缓存,你可以标记共享内容(例如工具和指令),以便在多个会话之间以及启动辅助智能体时复用。你并不总是需要同时使用两种缓存形式,但将二者结合可以为你提供一个可复用的起点,同时由 Bedrock 自动管理不断增长的对话。
新会话也可以复用该共享起点,而每个对话则基于各自缓存的对话历史继续构建。在本文中,我们从第一轮开始,然后展示如何将缓存内容延续到后续轮次、新工具以及跨子智能体使用。
你好,缓存世界
第一个收益是缓存提示词中的静态部分:即跨请求保持不变的工具和指令。这些示例使用你现有的 Amazon Bedrock 客户端、TOOLS 和 task。请将简写的 INSTRUCTIONS 字符串替换为你真实的策略和示例。可复用的前缀必须达到 1,024 个 token 才符合条件。
缓存静态内容时,请使用显式模式:由你选择保存的前缀在哪里结束。
KEY = "support-agent:v7:gpt-6-astra"
INSTRUCTIONS = "Your full support policies and examples..."
# Rendered context: [provider context][TOOLS][INSTRUCTIONS] | [task]
# Cache starts at the beginning; the marker below chooses its end.
resp = client.responses.create(
model="us.openai.gpt-6-astra",
store=False, # Response storage; prompt caching still works.
include=["reasoning.encrypted_content"],
prompt_cache_key=KEY,
prompt_cache_options={"mode": "explicit", "ttl": "30m"},
tools=TOOLS, # Rendered before instructions; keep order stable.
input=[
{
"role": "developer",
"content": [{
"type": "input_text",
"text": INSTRUCTIONS,
# Save through this whole block, including TOOLS.
"prompt_cache_breakpoint": {"mode": "explicit"},
}],
},
# User message is processed but not cached.
{"role": "user", "content": task},
],
)prompt_cache_key 告诉服务哪些请求旨在共享同一缓存。等我们讲到在新会话中读取缓存时,会再回来讨论如何使用它。在我们一项使用显式缓存的测试中,约 4,000 个 token 的工具与指令前缀在首次请求时写入了 4,120 个 token,并在后续请求中读取了这 4,120 个 token。task(或用户请求)位于缓存边界之后,因此不会触发对缓存的写入。
请记住,提示词缓存仅在你传入完全相同的文本时才有效。请确保智能体的指令和工具列表每次都以相同方式序列化。模型上下文协议(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},
]
# Automatic endpoint: the end of the user message.
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},
]
# Still implicit mode, now with an explicit startup save point in the history.
resp = ask_with_implicit_cache(history)缓存对话历史和工具结果
模型按名称请求工具并提供其参数。你的应用必须执行这些工具。execute_function 是你构建的辅助函数,用于将这些请求路由到匹配的 Python 函数并返回其结果。下面的循环使用该辅助函数运行每个请求的工具。它将模型的输出和所有工具结果追加到 history 列表中,然后再次发送该列表,直到模型返回最终答案。在隐式模式下,Amazon Bedrock 可以复用未更改的前缀,并缓存截至最后一个工具结果所添加的内容。保持较早的消息和初始 TOOLS 不变。
import json
# Map tool names to their Python implementations.
TOOL_HANDLERS = {
"get_owner": get_owner,
# Register your other initial tools here.
}
def execute_function(name, arguments):
"""Run the requested tool and return its result."""
handler = TOOL_HANDLERS.get(name)
if handler is None:
return {"error": f"Unknown tool: {name}"}
return handler(**arguments)
while True:
# Preserve all output items, including encrypted reasoning.
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),
})
# The last result in this group supplies the automatic endpoint.
resp = ask_with_implicit_cache(history)对于后续请求,追加一条新的用户消息并再次调用该辅助函数。保持较早的消息不变,以便其缓存端点仍然可用。
滚动断点:每一轮都会读取其之前的全部内容
图 3:滚动断点,每一轮读取其之前的全部内容,并且只写入自己的工具结果
对于单独的仅显式运行,从组合的 Hello cached world 示例重新创建两条消息的历史记录。此循环将工具结果包装在 input_text 块中,并标记每批中的最终结果。较早的标记保留在 history 中。有关长对话,请参阅当前查找规则。
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) # Includes encrypted reasoning.
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)}],
})
# Add the newest boundary; retain every earlier marker.
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": "Return the due date for a support case.",
"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": "Find the due date for case C-123.",
})
# Implicit mode saves through this new user message, including NEW_TOOL.
resp = ask_with_implicit_cache(history)
d = resp.usage.input_tokens_details
print("old prefix read", d.cached_tokens, "new write", d.cache_write_tokens)
# Example output:
# old prefix read 2048 new write 256会话中途出现新工具
图 5:追加一个工具后,之前的前缀仍可复用,示例计数来自 Amazon Bedrock Runtime 上的 GPT-6 Astra
用新的响应继续工具执行循环。该循环将模型输出添加到 history,运行任何请求的工具,并追加其结果。在后续请求中保留 additional_tools 项,以便新工具保持可用。在隐式模式下,Amazon Bedrock 可以缓存到新的用户消息,包括其前面的工具定义。在仅显式模式下,保留较早的断点,并在该用户消息中的 input_text 块上添加一个断点。additional_tools 项本身不能持有断点。
不同角色,不同目录。 当不同的 agent 角色需要不同的工具时,在每个会话开始时选择一个稳定的工具配置文件,例如支持或计费。每个配置文件都有自己的缓存前缀。标记共享指令,以便之后使用该配置文件的任务可以复用它们,然后随着对话增长使用任一模式。
缓存的工具配置文件
图 6:每个工具配置文件都有自己的缓存前缀,可由匹配的会话复用
子代理和侧调用
委派不应让每个助手重读相同的背景。共享前缀,然后在末尾添加每个助手的任务。请注意,当您将缓存与 AI agent 一起使用时,工具和系统提示可能会被泄露或利用。建立负责任的 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 contains the task text for each helper.
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:在启动助手之前预热共享前缀,然后每个助手添加自己的任务
给侧调用相同的开头。 agent 循环通过追加每个 resp.output 及其匹配的工具结果(包括加密推理)来构建 history。父级完成后,复制该完整列表给摘要器或评判器。同一助手保持工具、模型和密钥不变:
from copy import deepcopy
# history is the completed history built by the agent loop above.
# Copy it so the helper can add instructions without changing the parent.
helper_history = deepcopy(history)
helper_history.append({
"role": "developer",
"content": "Summarize this conversation in one sentence. Do not call tools.",
})
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)
# Example output (assumed values and conversation):
# history read 4096 write 32
# Case C-123 is due on October 31, 2026.助手可以复用 history 的缓存部分,但追加的开发者消息不会自动将缓存扩展到包含摘要指令。如果后续请求复用相同的历史和指令,请将指令放在带有 prompt_cache_breakpoint 的 input_text 块中。对于一次性摘要,不需要额外的标记。
暂停、重启和缓存键
暂停。 缓存前缀具有至少 30 分钟的生存时间(TTL),通过成功复用刷新,并且可能保持更长时间。如果会话很快恢复,普通预热可以在前缀过期前刷新它。将该请求的成本与让缓存过期进行比较。有关 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."),
]:
# A fresh conversation for each session; the startup prefix matches.
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 即使任务不同也能读取它。每个任务也可以被隐式缓存。检查使用计数器:匹配的键和标记使复用成为可能,但缓存条目仍必须可用。
提示缓存能带来什么
成本。对于一个由 GPT-6 Astra 驱动的智能体,具有 8,000 个 token 的启动前缀和六轮每轮增加 2,000 个 token 的对话,一次性静态缓存将成本降至 0.50 美元,而滚动断点将建模输入成本降低了 64%:从每会话 0.87 美元降至 0.32 美元。
图 9:支持智能体示例的每会话累计输入成本,使用 GPT-6 Astra 的 Amazon Bedrock 按需价格
如何判断缓存正在生效
从响应使用情况开始。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,并按 使用类型 分组。缓存读取、缓存写入和未缓存输入会按模型分别显示为单独的条目。在计算它们在输入成本中的份额时,请排除输出 token 条目。
一份简短清单
- 保持可复用前缀稳定。将共享工具和指令放在最前面,然后追加对话历史和新轮次。更改时间戳、消息或工具定义会阻止该更改之后的复用。如果较短的前缀有自己的已保存端点,它仍然可以被复用。
- 围绕你想要复用的内容选择边界。对不断增长的历史记录使用隐式缓存,对共享前缀使用显式标记,或两者兼用。仅显式模式可避免写入一次性后缀。在扩展仅显式对话时,保留较早的标记,以便后续请求能够找到已保存的前缀。
- 保持初始工具目录不变。使用
allowed_tools限制哪些工具可调用。使用additional_tools追加已知定义,或通过带命名空间的客户端tool_search发现它们。当不同智能体角色需要不同目录时,选择稳定的工具配置文件。 - 检查前缀大小和生命周期。可缓存前缀必须至少包含 1,024 个 token。缓存前缀在其最近一次写入或成功复用后至少 30 分钟内仍可复用。
- 在启动辅助程序之前预热共享内容。在预热请求和辅助请求中都标记共享前缀,包括使用隐式模式时。在开始并行调用之前完成预热请求。
- 在每个共享组内保持缓存键稳定。旨在复用某个前缀的请求必须使用相同的键。单独的键可以使不同客户或用户之间的缓存核算保持独立。
- 衡量整个工作流程中的节省。 缓存写入费用是正常输入费率的 1.25 倍。读取价格取决于模型。在开发过程中使用响应用量、在生产环境中使用监控和账单数据,将写入成本与读取节省一并跟踪。
2 月 3 日醒来
比尔·默里只有在不再重复同样的错误、并开始把从一天中学到的东西带到下一天时,才逃离了《土拨鼠之日》。智能体也以类似的方式实现这一点,尽管没那么浪漫:保持提示词开头逐字节稳定,并随着历史增长使用自动或显式缓存边界。
在本文中,我们介绍了如何缓存共享工具和指令、在智能体多轮交互中扩展缓存,以及在添加工具、启动辅助程序或开启新会话时保持复用。保持共享前缀稳定,并随着工作流程推进追加新信息。对不断增长的对话使用隐式缓存,对共享内容使用显式断点。两者都需要时可将它们结合使用,或者当变化的后缀不太可能被复用时选择仅显式模式。比较缓存读取、写入、总输入成本和延迟,找到最适合你的智能体的方案。要应用这些模式,请遵循 Amazon Bedrock 针对 OpenAI 模型的提示缓存文档,并查看 按 AWS 区域和端点的模型可用性。从一个工作流程开始,并随着对话增长衡量节省。
关于作者
Claudio Mazzoni
Claudio 是 Amazon Bedrock GTM 团队的高级专家解决方案架构师。Claudio 擅长引导客户完成其生成式 AI 之旅。工作之余,Claudio 喜欢与家人共度时光,并在自家花园里劳作。
来源:AWS Machine Learning Blog · aws.amazon.com








