土拨鼠日里的 AI 智能体:在 Amazon Bedrock 上为 OpenAI 模型使用提示缓存
说明智能体每一轮如何重发整段上下文,以及在 Amazon Bedrock 上用提示缓存复用稳定前缀、追加工具并核对成本。

目录
#土拨鼠日里的 AI 智能体:在 Amazon Bedrock 上为 OpenAI 模型使用提示缓存
每次你把智能体转起来,它在采取行动之前都得重读自己的整段人生。下面说明如何让它记住,而不必再喂一遍同样的上下文。
在电影《土拨鼠日》(Groundhog Day)里,Bill Murray 饰演的角色每天都在重过同一个 2 月 2 日。他醒来时收音机放着同一首歌,住在同一家旅店,身处同一个小镇,如此反复。有些影迷估计这段日子长达 34 年。
前一天做成的事,除非他从头再做一遍,否则带不到下一天。AI 智能体活在土拨鼠日的某个版本里。智能体每走一步(调用工具、读取结果、决定下一步),为了保持连续性,都必须把到目前为止的整段故事发给模型:工具定义、指令、每一次先前的工具调用,以及每一次结果。模型只根据你提供的输入预测下一个词元(token)。请求之间它没有内部记忆,所以必须把全部内容再读一遍。到编码会话的第 20 轮,模型会重读第 1 到 19 轮,而每一轮那些词元的费用,都由你一次又一次支付。
提示缓存(Prompt caching)打破这个循环。服务存储你的输入提示(指令、工具和内容),并在下一次请求中复用,输入价格只是原来的一小部分。
在本文中,我带你走过智能体的一生,从第一轮到它获得新工具并派生助手。沿途说明提示缓存如何压低成本,并让智能体保持响应和一致。
智能体向其 LLM 发送什么
在谈缓存之前,先看典型智能体第一次调用大语言模型(large language model,LLM)时包含什么:
- 工具列表 – 智能体可以调用的每个函数的定义。你发布新版本时会变。
- 指令 – 开发者提示:规则、人设、策略、示例。也按版本变化。
- 历史 – 本次会话到目前为止发生的一切:任务、模型的工具调用,以及工具结果(每一轮都在增长)。
- 新一轮 – 最新的工具结果或用户消息(每次都可能不同)。
如果不设置缓存,智能体一轮又一轮发给模型的内容:
图 1:智能体每一轮发给模型的内容
提示缓存在底层如何工作
模型读取提示时,首先做一次称为预填充(prefill)的写入。预填充期间,它处理每一个输入词元,并构建键向量和值向量(也称为键值,key-value,或 KV),它们构成模型的工作状态。这发生在模型写出第一个词元之前。
提示缓存保存该状态,其中包含系统提示、工具、内容、历史等等。当后来的请求以相同的输入词元开头(也就是相同的文本)时,提示缓存服务加载已保存的状态,而不是重新计算。然后它只处理发给模型的下一批负载中新增的部分。提示缓存可以是隐式的,由 Amazon Bedrock 选择可复用前缀在哪里结束;也可以是显式的,由你的代码用 prompt_cache_breakpoint 标记那个端点。两种做法可以一起用,我们也建议这样做。
第 1 轮与第 2 轮相比
图 2:第 1 轮把提示前缀保存到断点,第 2 轮复用它,只为新问题支付全价
隐式还是显式,为什么两者都需要?
对于智能体工作负载,隐式缓存让 Bedrock 在对话增长时选择缓存什么。用显式缓存,你可以标记共享内容,例如工具和指令,以便跨会话复用,以及在拉起助手智能体时复用。你并不总是需要两种缓存,但把它们结合起来,能给你一个可复用的起点,同时由 Bedrock 自动管理不断增长的对话。
新会话也可以复用那个共享起点,而每段对话在自己的缓存历史上继续构建。本文从第一轮开始,然后说明如何把缓存内容带到后续轮次、新工具,以及跨子智能体。
你好,被缓存的世界
第一处收益是缓存提示中的静态部分:跨请求保持不变的工具和指令。这些示例使用你现有的 Amazon Bedrock 客户端、TOOLS 和 task。把缩短的 INSTRUCTIONS 字符串换成你的真实策略和示例。可复用前缀必须达到 1,024 个词元才符合条件。
缓存静态内容时使用显式模式:选择已保存前缀在哪里结束。
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 词元的工具与指令前缀在第一次请求写入 4,120 个词元,在后来的请求中读取这 4,120 个词元。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:滚动断点,每一轮读取它之前的一切,只写入自己的工具结果
对于单独的仅显式运行,从组合的“你好,被缓存的世界”示例重建两条消息的历史。这个循环把工具结果包在 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 项本身不能持有断点。
不同角色,不同目录。 当不同智能体角色需要不同工具时,在每个会话开始时选择一个稳定的工具画像,例如支持或账单。每个画像有自己的缓存前缀。标记共享指令,使该画像的后续任务可以复用它们,然后在对话增长时使用任一模式。
已缓存的工具画像
图 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 中已缓存的部分,但追加的开发者消息不会自动把缓存延伸到包含摘要指令。如果后来的请求复用同一历史和指令,把指令放在带 prompt_cache_breakpoint 的 input_text 块中。对于一次性摘要,不需要额外标记。
暂停、重启和缓存键
暂停。 缓存前缀的存活时间(time to live,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."),
]:
# 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 词元、六轮各增加 2,000 词元的智能体,一次性静态缓存把成本降到 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 分组。缓存读取、缓存写入和未缓存输入按模型显示为不同条目。计算它们在输入成本中的份额时,排除输出词元条目。
一份简短检查清单
- 保持可复用前缀稳定。 先放共享工具和指令,再追加对话历史和新轮次。改变时间戳、消息或工具定义,会阻止该变更之后的复用。如果更短的前缀有自己的已保存端点,它仍然可以被复用。
- 围绕你想复用的内容选择边界。 对增长中的历史使用隐式缓存,对共享前缀使用显式标记,或两者都用。仅显式模式避免写入一次性的后缀。扩展仅显式对话时,保留更早的标记,以便后来的请求能找到已保存的前缀。
- 保持初始工具目录不变。 用
allowed_tools限制哪些工具可被调用。用additional_tools追加已知定义,或通过带命名空间的客户端tool_search发现它们。当不同智能体角色需要不同目录时,选择稳定的工具画像。 - 检查前缀大小和寿命。 可缓存前缀必须至少包含 1,024 个词元。缓存前缀在最近一次写入或成功复用之后,至少 30 分钟内仍有资格被复用。
- 在启动助手之前预热共享内容。 在预热请求和助手请求中都标记共享前缀,包括使用隐式模式时。在开始并行调用之前完成预热请求。
- 在每个共享组内保持缓存键稳定。 打算复用同一前缀的请求必须使用同一个键。分开的键可以让客户或用户之间的缓存记账保持区分。
- 在整个工作流上测量节省。 缓存写入的费用是正常输入费率的 1.25 倍。读取价格取决于模型。把写入成本与读取节省一起跟踪,开发时用响应用量,生产中用监控和账单数据。
在 2 月 3 日醒来
Bill Murray 只有在停止重复同样的错误、并开始把一天学到的东西带到下一天时,才离开土拨鼠日。智能体以相似、只是不那么浪漫的方式到达那里:让提示的开头逐字节保持稳定,并在历史增长时使用自动或显式的缓存边界。
本文说明了如何缓存共享工具和指令,如何跨智能体轮次增长缓存,以及在添加工具、启动助手或开始新会话时如何保持复用。保持共享前缀稳定,并在工作流推进时追加新信息。对增长中的对话使用隐式缓存,对共享内容使用显式断点。需要两者时把它们组合,或在变化的后缀不太可能被复用时选择仅显式模式。比较缓存读取、写入、总输入成本和延迟,找出对你的智能体最有效的做法。要应用这些模式,遵循 OpenAI 模型的 Amazon Bedrock 提示缓存文档,并查看按 AWS 区域和端点的模型可用性。从一个工作流开始,并在对话增长时测量节省。
PRIMARY_SOURCE: https://aws.amazon.com/blogs/machine-learning/groundhog-day-for-ai-agents-prompt-caching-patterns-for-using-openai-models-on-amazon-bedrock
- #提示缓存
- #Amazon Bedrock
- #智能体
- #成本
- #检查清单


