AI 智能体的“土拨鼠之日”:在 Amazon Bedrock 上使用 OpenAI 模型的提示词缓存模式
每次启动智能体,它都得把自己的“前半生”从头读一遍,才能开始干活。这篇讲的就是怎么让它记住上下文,不用每次都把同样的内容重新喂一遍。
电影《土拨鼠之日》(Groundhog Day)里,比尔·默瑞的角色每天都在重复 2 月 2 日。他在同一家旅馆醒来,广播里放着同一首歌,面对同一个小镇,日复一日——有影迷估算,这循环持续了 34 年。前一天做过的事,他一点也没法带到下一天,除非从头再做一遍。AI 智能体也活在类似的“土拨鼠之日”里。
智能体每走一步(调用工具、读取结果、决定下一步),都必须把“到目前为止的全部故事”发给模型,才能保持连贯:工具定义、指令、之前每一次工具调用、以及每一个结果。模型只能根据你给的输入预测下一个词(token)。请求之间,它没有内部记忆,所以这些内容只能每次都重新读。写代码的会话进行到第 20 轮时,模型把第 1 到第 19 轮又重读了一遍——而每一个 token 都是你在付费,一遍又一遍。
提示缓存(Prompt caching)能打破这个循环。服务端会把你的输入提示(指令、工具和内容)存起来,下一次请求直接复用,输入价格只需原来的一小部分。这篇里,我会带你走一遍智能体的一生:从第一轮调用,到它拿到新工具、再拉起帮手。过程中我会说明,提示缓存如何压低成本,并让智能体保持响应速度和一致性。
智能体发给 LLM 的内容
讲缓存之前,先看看一个典型智能体第一次调用大语言模型(LLM)时都发了什么:
- 工具列表:智能体能调用的每个函数的定义。发布新版本时会变。
- 指令:开发者提示词,包含规则、人设、策略、示例。每次发布也可能变。
- 历史:本次会话到目前为止发生的一切——任务、模型的工具调用、工具返回的结果,而且每一轮都会增长。
- 新一轮输入:最新的工具结果或用户消息,每次可能都不一样。
如果不设置缓存,智能体每一轮发给模型的内容会是这样的:
图 1:智能体每一轮都发送给模型的完整内容
提示缓存的底层原理
模型读入提示时,会先做一次写入,称为预填充(prefill)。预填充阶段,模型逐个处理输入 token,构建键值向量(key-value vectors,简称 KV),这些向量构成了模型的工作状态。这些都在模型生成第一个 token 之前完成。
提示缓存保存的就是这个状态,包括系统提示、工具定义、内容、历史记录等等。之后发起请求时,如果开头的输入 token 与之前一致(也就是同样的文本),提示缓存服务会直接加载已保存的状态,不必重新计算,只处理新加入的部分。
缓存有两种方式:隐式缓存,由 Amazon Bedrock 自动判断可复用的前缀到哪里结束;显式缓存,由你的代码通过 prompt_cache_breakpoint 标记截止位置。两种方式可以同时使用,我们也推荐这样做。
第 1 轮和第 2 轮的对比:
图 2:第 1 轮保存断点之前的提示前缀,第 2 轮直接复用,只为新问题付费
隐式还是显式,为什么两种都要?
在智能体场景里,对话会不断变长,隐式缓存让 Bedrock 自动选择要缓存的部分。显式缓存则适合标记共享内容,比如工具定义和指令,让它们在多个会话之间复用,也可以在启动辅助智能体时复用。
并不是每次都需要两种缓存一起用,但组合起来效果更好:你有一个可复用的起点,同时 Bedrock 自动管理不断增长的对话。新会话可以复用这个共享起点,而每个对话又能在各自的缓存历史上独立构建。
本文从第 1 轮讲起,然后说明如何在后续轮次、新增工具以及子智能体之间延续缓存内容。
Hello cached world
第一个收益点,是缓存提示中的静态部分:那些在每次请求中保持一致的工具定义和指令。下面的示例基于你现有的 Amazon Bedrock 客户端、TOOLS 和 task。请把简化的 INSTRUCTIONS 字符串替换为真实的策略和示例。
可复用的前缀至少要达到 1,024 个 token 才能生效。缓存静态内容时,应使用显式模式:由你指定缓存前缀的结束位置。KEY = "support-agent:v7:gpt-6-astra"
INSTRUCTIONS = "你的完整支持政策和示例..."
# 渲染后的上下文:[provider context][TOOLS][INSTRUCTIONS] | [task]
# 缓存从开头开始;下面的标记决定其结束位置。
resp = client.responses.create(
model="us.openai.gpt-6-astra",
store=False, # 响应存储;prompt caching 仍然生效。
include=["reasoning.encrypted_content"],
prompt_cache_key=KEY,
prompt_cache_options={"mode": "explicit", "ttl": "30m"},
tools=TOOLS, # 在 instructions 之前渲染;保持顺序稳定。
input=[
{
"role": "developer",
"content": [{
"type": "input_text",
"text": INSTRUCTIONS,
# 缓存整个块,包括 TOOLS。
"prompt_cache_breakpoint": {"mode": "explicit"},
}],
},
# 用户消息会被处理,但不会缓存。
{"role": "user", "content": task},
],
prompt_cache_key 用来告诉服务:哪些请求应该共用同一份缓存。等后面讲到在新会话里读取缓存时,我们再回头说它的用法。
在显式缓存(explicit caching)的一次测试中,那个约 4,000 个 token 的「工具 + 指令」前缀在第一次请求时写入了 4,120 个 token,后续请求则直接读取这 4,120 个 token。任务本身(也就是用户请求)落在缓存边界之后,所以不会触发写缓存。
要注意:prompt 缓存只有在文本完全一致时才生效。务必保证 agent 的指令和工具列表每次都用相同方式序列化。Model Context Protocol(MCP,模型上下文协议)服务器以及其他形式的工具投递方式,都要建立索引,确保它们保持稳定。凡是会变的内容,比如日期、用户名或请求 ID,都要放在已缓存的指令之后。
随着内容不断增长和变化(工具不同、内容不同),可以改用隐式模式(implicit mode),让服务自己跟随对话。这时第一次请求没有显式标记,它最新的可缓存消息就是用户的任务,于是自动断点落在该任务之后,把工具、指令和任务一起覆盖进去。隐式模式本来就是默认值,为了看得更清楚,下面的示例仍然把它显式写出来。
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, # 全部内容都会被缓存
)
history = [
{"role": "developer", "content": INSTRUCTIONS},
{"role": "user", "content": task},
]
resp = ask_with_implicit_cache(history)
当下一轮对话延续同一任务时,这种方式很有用。如果换成另一项任务,就无法匹配到经过旧任务的前缀,而且隐式写入未必会单独为工具和指令创建一条记录。在这种情况下,如果你需要让这段较短的共享前缀能跨任务复用,就应该改用显式标记。当智能体既需要共享起点、又需要不断增长的历史记录时,可以两者兼用。
先看下面代码示例里带标记的 developer 块,然后调用 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 函数并返回结果。下面的循环用这个辅助函数逐个运行模型请求的工具。它把模型的输出和所有工具结果追加到 history 列表,然后再次发送这个列表,直到模型给出最终答案。在隐式模式下,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,然后把它的 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)
# old prefix read 2048 new write 256
会话中途新增工具
图 5:追加一个工具后,原有前缀依然可以复用;示例计数来自 Amazon Bedrock Runtime 上的 GPT-6 Astra
拿到新响应后,继续工具执行循环。所谓循环,就是把模型输出追加到 history,运行它请求的工具,再把工具结果追加进去。后续请求里要保留 additional_tools 这一项,新加的工具才能一直可用。
- 隐式模式下,Amazon Bedrock 可以把缓存一直铺到新增的那条用户消息,包括它前面的工具定义。
- 只用显式模式时,保留已有的断点,再在那条用户消息的某个 input_text 块上加一个断点。additional_tools 这一项本身不能放断点。
角色不同,工具目录也不同
不同智能体角色需要不同工具时,在每次会话开始时选定一套固定的工具配置(tool profile),比如「客服」或「计费」。每套配置各自有一份缓存前缀。把共用的指令标记出来,后面用同一配置的任务就能复用;之后随对话变长,两种模式任选其一。
图 6:每套工具配置都有自己的缓存前缀,可由配置相同的会话复用
子智能体与旁路调用
把任务分派出去,不该让每个帮手都把同一份背景重读一遍。共享前缀,再把各自的任务加在末尾。要注意:智能体配合缓存使用时,工具和系统提示有可能被泄露或被利用。请建立负责任的 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))
这些 helper 沿用预热时的同一套 TOOLS、同一个模型和同一个 KEY。预热即使撞到输出上限提前结束,也照样能把缓存填上。启动 helper 之前,先用 usage 数据确认缓存确实生效。
子代理:共享前缀预热一次,再分发给多个子代理
图 7:启动 helper 之前先预热共享前缀,之后每个 helper 再各自附加自己的任务
如果想让旁路调用(side call)拥有相同的开头,做法也很简单。上文的 agent 循环每轮都会把 resp.output 和对应的工具结果追加进历史,其中也包括加密的推理内容。父级跑完后,把这份完整历史复制一份,交给摘要器或评审器。helper 的 tools、model、key 依然保持不变:
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
案例 C-123 的截止日期是 2026 年 10 月 31 日。helper 可以复用缓存中的历史部分,但追加的 developer 消息不会自动把缓存扩展到包含摘要指令。如果后续请求要复用相同的历史和指令,就把指令放进 input_text 块,并设置 prompt_cache_breakpoint。如果只是一次性摘要,不需要额外标记。
暂停、重启与缓存键
暂停。缓存前缀的最短存活时间(TTL)为 30 分钟,成功复用会刷新 TTL,实际保留时间可能更长。如果会话很快恢复,普通的预热请求就能在缓存过期前刷新前缀。可以对比这个请求的成本和让缓存过期的成本。关于 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."),
]:
# 每个会话都开一段新对话;开头的 prefix 保持一致
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 驱动的 agent 为例:启动前缀 8,000 个 token,六轮对话每轮增加 2,000 个 token。一次性静态缓存把成本降到 0.50 美元;滚动断点让建模输入成本降低 64%:从每会话 0.87 美元降到 0.32 美元。
图 9:支持 agent 示例的每会话累计输入成本,使用 Amazon Bedrock 按需价格(GPT-6 Astra)
如何判断缓存是否生效
先看响应的 usage。cached_tokens 报告从缓存读取的输入,cache_write_tokens 报告写入缓存的输入。随着对话增长,检查后续轮次是否复用较早的历史,以及新的写入是否反映新增内容。
在生产环境中监控 Amazon Bedrock Runtime 端点时,使用 Amazon CloudWatch 中的 CacheReadInputTokenCount 和 CacheWriteInputTokenCount 指标。需要单条请求记录时,启用模型调用日志,然后用 Amazon CloudWatch Logs Insights 查询。
对于 Amazon Bedrock Mantle 端点,把响应中的 usage 字段记录到应用日志里。它的 Amazon CloudWatch 命名空间目前没有单独的缓存计数器。
检查成本时,在 AWS Cost Explorer 中筛选 Amazon Bedrock,并按 Usage type 分组。缓存读取、缓存写入和未缓存输入会按模型分别显示为独立条目。计算它们占输入成本的比例时,排除输出 token 条目。
简短清单
- 保持可复用前缀稳定。 把共享的工具和指令放在前面,然后追加对话历史和新轮次。改动时间戳、消息或工具定义,都会让该改动之后的缓存无法复用。如果较短的前缀有自己的已保存端点,仍然可以复用。
- 围绕想复用的内容选择边界。 不断增长的历史用隐式缓存,共享前缀用显式标记,或者两者兼用。
仅显式模式可以避免写入一次性后缀。延长仅显式对话时,保留之前的标记,让后续请求能找到已保存的前缀。初始工具目录保持不变,用 allowed_tools 限制可调用的工具。已有定义用 additional_tools 追加,或者通过带命名空间的客户端工具 tool_search 来发现。不同智能体角色需要不同目录时,选择一个稳定的工具配置。检查前缀大小和生命周期:可缓存前缀至少要 1,024 个 token。缓存前缀在最近一次写入或成功复用后,至少 30 分钟内仍可复用。启动辅助进程前先预热共享内容,在预热请求和辅助请求里都标记共享前缀,使用隐式模式时也一样。预热请求完成后再发起并行调用。每个共享组内保持缓存键稳定,打算复用同一前缀的请求必须使用同一个键。不同的键可以让缓存计费在客户或用户之间保持独立。衡量整个工作流的节省效果:缓存写入按正常输入价格的 1.25 倍计费,读取价格取决于模型。把写入成本和读取节省放在一起看,开发阶段用响应里的用量数据,生产环境用监控和账单数据。
2 月 3 日醒来
Bill Murray 只有在停止重复同样的错误、开始把每天学到的东西带到下一天之后,才逃出了《土拨鼠之日》。智能体抵达终点的过程类似,只是没那么浪漫:让提示的开头保持逐字节稳定,随着历史增长使用自动或显式缓存边界。本文介绍了如何缓存共享的工具和指令、在智能体多轮对话中扩展缓存,以及在添加工具、启动辅助进程或开启新会话时保持复用。让共享前缀保持稳定,随着工作流推进追加新信息。多轮增长的对话用隐式缓存,共享内容用显式断点。两者都需要时组合使用;如果变化的后缀不太可能被复用,就选仅显式模式。
对比缓存读取、缓存写入、总输入成本和延迟,找到最适合你的智能体的方案。要应用这些模式,请参阅 Amazon Bedrock 针对 OpenAI 模型的提示缓存文档,并按 AWS 区域和端点查看模型可用性。先从一个工作流入手,随着对话量增长再逐步衡量节省效果。
关于作者
Claudio Mazzoni
Claudio 是 Amazon Bedrock GTM 团队的高级专家解决方案架构师。他擅长引导客户走好生成式 AI 之旅。工作之余,Claudio 喜欢陪伴家人、打理自家花园。
智能体每走一步,都要先把整段上下文重新读一遍,然后才行动。本文介绍 Amazon Bedrock 上 OpenAI 模型的提示缓存(prompt caching)如何让智能体「记住」内容:不必每一轮都把相同的工具、指令和历史重新发一遍,从而降低输入成本、减少延迟。