AI 智能体的“土拨鼠之日”:在 Amazon Bedrock 上使用 OpenAI 模型的提示词缓存模式

AWS ML Blog 2026-10-26T21:57:09.281472

每次启动智能体,它都得把自己的“前半生”从头读一遍,才能开始干活。这篇讲的就是怎么让它记住上下文,不用每次都把同样的内容重新喂一遍。

电影《土拨鼠之日》(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:滚动断点,每轮读取之前的所有内容,只写入自己的工具结果

图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 上各缓存方案的输入成本(相对不缓存)

注意事项:

工具发生变化时

有时智能体的工具列表会变化,或新增工具。保持初始列表稳定:增加、删除或重排定义都可能导致缓存前缀失效。根据工具可用的方式选择合适策略。

当已知工具在智能体流程的不同步骤中被调用时:发送相同的已排序 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 这一项,新加的工具才能一直可用。

角色不同,工具目录也不同

不同智能体角色需要不同工具时,在每次会话开始时选定一套固定的工具配置(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)如何让智能体「记住」内容:不必每一轮都把相同的工具、指令和历史重新发一遍,从而降低输入成本、减少延迟。

查看原文