Amazon Bedrock 上的 OpenAI 模型提示词缓存实践:从静态前缀到子代理复用
Groundhog Day for AI agents: Prompt caching patterns for using OpenAI models on Amazon Bedrock
AWS 在 Amazon Bedrock 上讲解如何用提示词缓存避免 Agent 每轮重复读取全部上下文。
每次你分离出一个新的智能体,它在执行任何操作之前都必须重新读取自己完整的“人生经历”。本文将介绍如何让它记住上下文,而不必每次都重新喂入同样的内容。
在电影《土拨鼠之日》中,比尔·默瑞饰演的角色日复一日地重复经历着同一个2月2日。他每天醒来,收音机里播放着同一首歌,身处同一家旅店、同一个小镇,如此循环往复——据一些影迷估算,长达34年。
他前一天取得的任何成果都无法延续,除非他从头再来一遍。AI 智能体就生活在这样一个“土拨鼠之日”的版本里。每次智能体执行一步操作(调用工具、读取结果、决定下一步做什么),它都必须把自己的全部历史发送给模型以保持连贯性:工具定义、指令、之前每一次工具调用以及每一个结果。模型只会根据你提供的输入来预测下一个 token。它在请求之间没有任何内部记忆,因此必须把所有内容重新读一遍。到了编码会话的第20轮,模型要重新阅读第1到19轮的内容,而这些 token 的费用全都要由你来买单,一次又一次。
提示词缓存(prompt caching)打破了这个循环。该服务会存储你的输入提示词(指令、工具和内容),并在下一次请求时复用它,而输入价格只需原来的一小部分。
在这篇文章中,我会带你了解一个智能体的一生,从它的第一轮对话,到它获得新工具并生成辅助智能体的过程。在此过程中,我会向你展示提示词缓存如何降低成本,并让智能体保持响应迅速和行为一致。
智能体向其 LLM 发送了什么
在讨论缓存之前,先来看看一个典型智能体首次调用大语言模型(LLM)时包含的内容会很有帮助:
- 工具列表 – 智能体可调用的每个函数的定义。发布新版本时会发生变化。
- 指令 – 开发者提示词:规则、人设、策略、示例。每次发布也会变化。
- 历史记录 – 本次会话中迄今为止发生的一切:任务、模型的工具调用以及工具结果(每一轮都在增长)。
- 新的一轮 – 最新的工具结果或用户消息(每次可能都不同)。
如果不设置缓存,智能体在一轮又一轮中会向模型发送什么:
图1:智能体在每一轮向模型发送的内容
提示词缓存在底层是如何工作的
当模型读取提示词时,它首先会执行一个称为 prefill(预填充)的写入操作。在预填充期间,模型会处理每个输入 token,并构建其键向量和值向量(也称为键值对,即 KV),它们构成了模型的工作状态。这发生在模型写出第一个 token 之前。
提示词缓存会保存这个状态,其中包含系统提示词、工具、内容、历史记录等等。当后续请求以相同的输入 token(即相同的文本)开始时,提示词缓存服务会加载已保存的状态,而不再重新计算。随后它只需处理下一个负载中新增的内容。提示词缓存可以是隐式的,由 Amazon Bedrock 自动选择可复用前缀的结束位置;也可以是显式的,由你的代码使用 prompt_cache_breakpoint 来标记该端点。这两种方式可以结合使用,我们也建议这样做。
第1轮与第2轮的对比
图2:第1轮将提示词前缀保存到断点处,第2轮复用该前缀,并只需为新问题支付全价
隐式还是显式?为什么两者都需要?
对于智能体工作负载,隐式缓存允许 Bedrock 在对话增长时自行选择要缓存的内容。通过显式缓存,你可以标记共享内容(例如工具和指令),以便跨会话以及在启动辅助智能体时重复使用。你并不总是需要同时使用两种缓存方式,但将它们结合使用可以在 Bedrock 自动管理不断增长的对话的同时,为你提供一个可复用的起点。
新会话也可以复用这个共享起点,同时每个对话在各自缓存的历史记录之上继续构建。在这篇文章中,我们从第一轮对话开始,然后展示如何将缓存内容延续到后续轮次、新工具以及子智能体中。
缓存“Hello, cached world”
第一步收益是缓存提示词的静态部分:即在请求之间保持不变的工具和指令。这些示例使用你现有的 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(或用户请求)位于缓存边界之后,因此这不会触发向缓存的写入。
请记住,提示词缓存只有在传入的文本完全相同时才生效。确保智能体的指令和工具列表每次都以相同的方式进行序列化。Model Context Protocol (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,然后将其 schema 追加到智能体循环已完成的历史中。保持初始 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 项本身不能持有断点。
不同角色,不同目录。当不同的智能体角色需要不同的工具时,请在每个会话开始时选择一个稳定的工具配置,例如 support 或 billing。每个配置拥有自己的缓存前缀。标记共享的指令,以便后续使用该配置的任务能够复用它们,然后随着对话的增长使用任意一种模式。
缓存的工具配置
图 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 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:在启动辅助智能体之前预热共享前缀,然后每个辅助智能体添加自己的任务
让旁路调用拥有相同的开头。智能体循环通过追加每个 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 的缓存部分,但追加的 developer 消息不会自动扩展缓存以包含摘要指令。如果后续请求复用相同的历史和指令,请将该指令放入一个带有 prompt_cache_breakpoint 的 input_text 块中。对于一次性摘要,无需额外的标记。
暂停、重启与缓存键
暂停。缓存前缀的最短生存时间(TTL)为 30 分钟,成功的复用会刷新它,并且可能保留更长时间。如果会话很快恢复,普通的预热可以在前缀过期之前刷新它。将该请求的成本与让缓存过期进行比较。有关 TTL 规则,请参阅 Amazon Bedrock 文档。
新会话。对于旨在共享前缀的请求,保持 prompt_cache_key 稳定。特定于会话的键会将各会话隔离开来,即使它们的提示匹配。在以下示例中,切换到共享键使各会话能够复用前缀。
新会话仅在共享键时才复用缓存
图 8:特定于会话的键会将缓存条目分隔开,而共享键使匹配的会话能够复用它们
复用现有缓存内容需要相同的模型、AWS 账户和区域(Region),以及可用的匹配前缀。下面的示例将显式启动标记与隐式模式相结合。每个会话都以相同的指令和各自的任务开始。两者使用相同的计算键:
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 并按 Usage type(使用类型) 分组。缓存读取、缓存写入和未缓存输入会按模型分别显示为独立条目。在计算它们占输入成本的比例时,请排除输出 token 条目。
一份简明清单
- 保持可复用前缀稳定。将共享的工具和指令放在最前面,然后附加对话历史和新的对话轮次。更改时间戳、消息或工具定义会使该更改之后的内容无法复用。较短的前缀如果拥有自己已保存的端点,仍可被复用。
- 围绕想要复用的内容选择边界。对不断增长的对话历史使用隐式缓存,对共享前缀使用显式标记,或两者兼用。仅显式模式可避免写入一次性的后缀内容。在扩展仅显式模式的对话时,保留较早的标记,以便后续请求能找到已保存的前缀。
- 保持初始工具目录不变。使用
allowed_tools限制可调用的工具。通过additional_tools附加已知定义,或通过带命名空间的客户端tool_search发现它们。当不同的智能体角色需要不同的目录时,请选择稳定的工具配置。 - 检查前缀大小和生命周期。可缓存的前缀必须至少包含 1,024 个 token。已缓存的前缀在其最近一次写入或成功复用后至少 30 分钟内仍可被复用。
- 在启动辅助任务前预热共享内容。在预热请求和辅助请求中都标记共享前缀,包括使用隐式模式的情况。在开始并行调用之前完成预热请求。
- 在每个共享组内保持缓存键稳定。打算复用某个前缀的请求必须使用相同的键。使用不同的键可以在客户或用户之间保持缓存核算相互独立。
- 衡量整个工作流程中的节省。缓存写入的费用是正常输入费率的 1.25 倍。读取价格取决于模型。请同时跟踪写入成本与读取节省:开发阶段使用响应的 usage 数据,生产环境则使用监控与账单数据。
2 月 3 日醒来
比尔·默瑞只有在不再重复同样的错误、并开始把前一天学到的经验带到下一天时,才能逃离《土拨鼠之日》。智能体以类似(虽然没那么浪漫)的方式达到这一点:保持提示词开头部分逐字节稳定,并随着历史记录的增长使用自动或显式缓存边界。
在这篇文章中,我们介绍了如何缓存共享的工具和指令、跨智能体轮次扩展缓存,以及如何在添加工具、启动辅助智能体或开启新会话时保持复用。保持共享前缀稳定,并随工作流程的推进追加新信息。对于不断增长的对话使用隐式缓存,对共享内容使用显式断点。两者都需要时可以结合使用;如果后缀变化的部分不太可能被复用,则可以选择仅显式模式。比较缓存读取、写入、总输入成本和延迟,找出最适合你的智能体的方案。要应用这些模式,请参阅 Amazon Bedrock OpenAI 模型提示词缓存文档,并查看按 AWS 区域和端点划分的模型可用性。从一个工作流程开始,随着对话的增长衡量节省效果。
关于作者
来源: AWS Machine Learning Blog · aws.amazon.com








