AIsaAIsaStudio English 打开 AIsa Studio

AWS 详解 Amazon Bedrock 上 OpenAI 模型的 agent 提示词缓存模式

原标题:Groundhog Day for AI agents: Prompt caching patterns for using OpenAI models on Amazon Bedrock

AWS 工程师介绍在 Amazon Bedrock 上对 GPT-6 Astra 等 OpenAI 模型使用提示词缓存的模式,通过缓存 agent 每轮重复发送的工具定义、指令和历史,六轮工作负载输入成本降低约 62%。文章覆盖显式与隐式缓存、会话中途新增工具、子代理预热和缓存键设计,静态前缀最低需 1,024 token,TTL 至少 30 分钟。

为什么值得看

原文用 agent 六轮工作负载给出了显式与隐式缓存的实测成本对比,可帮助读者为自家 agent 选择缓存策略。

阅读原文 ↗10月27日 AI 日报

本页的摘要、推荐理由与解读出自 AIsa Studio 的 AI 风向智能体,原文版权归原作者所有。

深度解读

发生了什么

AWS Machine Learning 发文详解在 Amazon Bedrock 上使用 OpenAI 模型(如 GPT-6 Astra)时的 agent 提示词缓存模式。文章指出,agent 每一步都要把完整的工具定义、指令和历史记录重新发给模型,作者借电影《土拨鼠之日》类比这种「每次都要重读整个人生故事」的处境。

关键细节

  • 缓存原理:模型在 prefill 阶段构建 KV 状态,缓存服务保存该状态;后续请求若开头 token 相同,就加载已存状态,只处理新增内容。
  • 两种模式:implicit 由 Bedrock 自动选择可复用前缀的终点;explicit 由代码用 prompt_cache_breakpoint 标记边界,两者可结合使用。可缓存前缀至少 1,024 token,TTL 至少 30 分钟且可被成功复用刷新。
  • 代码示例展示:缓存静态的 TOOLS 与 INSTRUCTIONS;在会话中追加新工具(additional_tools)而不破坏已有前缀;用 allowed_tools 限制可调用工具;warm-up 后再并发启动 helper 子代理;通过稳定的 prompt_cache_key 让新会话复用共享前缀。
  • 效果数据:某六轮负载测试中,GPT-6 Astra 隐式缓存输入成本降约 62%,显式 rolling breakpoints 降约 61%;一个 8,000 token 启动前缀、六轮各增 2,000 token 的示例中,rolling breakpoints 将每会话建模输入成本从 $0.87 降到 $0.32。缓存写入价格为普通输入的 1.25 倍。

为什么重要

无缓存时,「到第 20 轮,模型要重读第 1–19 轮,而且是你为每一轮的 token 付费」。缓存显著降低长对话 agent 的输入成本与延迟,同时文章给出工程要点:前缀必须逐字节稳定,变化的日期、用户名等要放在缓存边界之后;显式模式要保留旧断点;生产环境可用 CloudWatch 指标验证缓存命中。

对谁有影响

主要影响在 Amazon Bedrock 上构建 agent、使用 OpenAI 模型的开发者与运维团队,尤其是多轮工具调用、子代理并发和长会话场景的团队,需要按各自工作负载实测节省。

全文翻译

以下翻译只覆盖原文的前一部分,其余内容请看下方原文。

每次你启动一个智能体(agent),它都必须在执行操作之前重新阅读其整个“人生故事”。以下是如何让它记住上下文,而无需再次提供相同的上下文。

在电影《土拨鼠之日》(Groundhog Day)中,比尔·默瑞饰演的角色每天重复经历同一个 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 轮的对比

第 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..."

# 渲染后的上下文:[provider context][TOOLS][INSTRUCTIONS] | [task]
# 缓存从开头开始;下面的标记选择其结束位置。
resp = client.responses.create(
    model="us.openai.gpt-6-astra",
    store=False,  # 响应存储;提示缓存仍然有效。
    include=["reasoning.encrypted_content"],
    prompt_cache_key=KEY,
    prompt_cache_options={"mode": "explicit", "ttl": "30m"},
    tools=TOOLS,  # 在指令之前渲染;保持顺序稳定。
    input=[
        {
            "role": "developer",
            "content": [{
                "type": "input_text",
                "text": INSTRUCTIONS,
                # 保存整个块,包括 TOOLS。
                "prompt_cache_breakpoint": {"mode": "explicit"},
            }],
        },
        # 用户消息会被处理但不会被缓存。
        {"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,  # 所有内容都会被缓存
    )

history = [
    {"role": "developer", "content": INSTRUCTIONS},
    {"role": "user", "content": task},
]
# 自动端点:用户消息的结尾。
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},
]
# 仍然是隐式模式,现在历史记录中包含一个显式的启动保存点。
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:滚动断点,其中每一轮读取其之前的所有内容,并仅写入其自身的工具结果

对于单独的全显式运行,从组合的“你好,缓存的世界”示例中重建两条消息的历史记录。此循环将工具结果包装在 input_text 块中,并标记每批中的最终结果。早期的标记保留在 history 中。请参阅当前的查找规则以获取长对话的信息。

import json

while True:
    resp = client.res
查看原文

Every time you spin off your agent, it has to re-read its whole life story before performing an action. Here’s how to make it remember without having to feed it the same context again.

In the movie Groundhog Day, Bill Murray’s character re-lives the same February 2 every day. He wakes up to the same song playing on the radio, in the same inn, and the same small town, again and again for what some movie fans estimate to be 34 years.

Nothing he accomplished the day before carries over unless he works it out again from scratch. AI agents live in a version of Groundhog Day. Every time an agent takes a step (calls a tool, reads the result, decides what to do next), it has to send the model its entire story so far to maintain continuity: the tool definitions, the instructions, every previous tool call, and every result. The model only predicts the next token based on the input you provide. It has no internal memory between requests, so it must read all of it again. By turn 20 of a coding session, the model re-reads turns 1–19, and it’s you who pays for every one of those tokens, again and again.

Prompt caching breaks the cycle. The service stores your input prompt (instructions, tools, and content) and reuses it on the next request, at a fraction of the input price.

In this post, I walk you through an agent’s life, from its first turn to when it gets new tools and spawns helpers. Along the way, I show you how prompt caching keeps costs down and keeps agents responsive and consistent.

What an agent sends to its LLM

Before talking about caching, it helps to look at what a typical agent’s first invocation to the large language model (LLM) contains:

  • Tool list – The definitions of every function the agent can call. Changes when you ship a new release.
  • Instructions – The developer prompt: rules, persona, policies, examples. Also changes per release.
  • History – Everything that happened in this session so far: the task, the model’s tool calls, and the tool results (which grows every turn).
  • New turn – The latest tool result or user message (which might be different every time).

What an agent sends to the model, turn after turn if you don’t set up caching:

Five growing agent requests, with tools, instructions, and earlier history marked in red and each new turn in yellow.

Figure 1: What an agent sends to the model on every turn

How prompt caching works under the hood

When a model reads a prompt, it first does a write called prefill. During prefill, it processes every input token and builds its key and value vectors (also known as key-value, or KV), which form the model’s working state. This happens before the model writes its first token.

Prompt caching saves that state, which contains the system prompt, tools, content, history, and so on. When a later request starts with the same input tokens (that is, the same text), the prompt caching service loads the saved state instead of computing it again. It then processes only what is new in the next payload to the model. Prompt caching can be implicit, where Amazon Bedrock selects where the reusable prefix ends, or explicit, where your code marks that endpoint using prompt_cache_breakpoint. You can use both approaches together, and we recommend doing so.

Turn 1 compared to turn 2

Turn 1 writes the tools and instructions through a breakpoint. Turn 2 reads that same prefix from cache and processes its new question.

Figure 2: Turn 1 saves the prompt prefix up to the breakpoint, and turn 2 reuses it and pays full price only for the new question

Implicit or explicit, and why do we need both?

For agentic workloads, implicit caching lets Bedrock choose what to cache as the conversation grows. With explicit caching, you can mark shared content, such as tools and instructions, for reuse across sessions and when spinning up helper agents. You don’t always need both forms of caching, but combining them gives you a reusable starting point while Bedrock manages the growing conversation automatically.

New sessions can also reuse that shared starting point while each conversation builds on its own cached history. In this post, we start with the first turn, then show how to carry cached content through later turns, new tools, and across subagents.

Hello cached world

The first win is to cache the static part of the prompt: the tools and instructions that stay the same across requests. These examples use your existing Amazon Bedrock client, TOOLS, and task. Replace the shortened INSTRUCTIONS string with your real policies and examples. The reusable prefix must reach 1,024 tokens to qualify.

When caching static content, use explicit mode: choose where the saved prefix ends.

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},
    ],
)

The prompt_cache_key tells the service which requests are meant to share a cache. We come back to how to use it when we get to reading the cache in new sessions. In one of our tests using explicit caching, the roughly 4,000-token tools-and-instructions prefix wrote 4,120 tokens on the first request and read those 4,120 tokens on later requests. The task (or user request) comes after the cache boundary, so this doesn’t trigger a write to the cache.

Keep in mind that prompt caching works only when you pass exactly the same text. Make sure that the agent’s instructions and tool lists are serialized the same way every time. Model Context Protocol (MCP) servers and other forms of tool delivery need to be indexed to make sure they stay consistent. Keep anything that varies, such as the date, the user’s name, or a request ID, after the cached instructions.

As the content grows and varies (different tools and content), use implicit mode to let the service follow the conversation. Here, the first request has no explicit marker. Its latest eligible message is the user’s task, so the automatic breakpoint comes after that task and covers the tools, instructions, and task together. The mode is shown explicitly in the following example for clarity, even though implicit is the default. Setting store=False leaves conversation history in your application. It doesn’t disable caching.

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)

This is useful when the next turn continues the same task. A different task doesn’t match the prefix through the old task, and the implicit write doesn’t necessarily create a separate entry for only the tools and instructions. Use an explicit marker there when you need that shorter shared prefix to be reusable across tasks.

Use both when the agent needs a shared starting point and a growing history. Start with the marked developer block in the following code example and call the function ask_with_implicit_cache. The request-level mode stays implicit. The block-level marker adds an explicit save point after the instructions. The service can also save through the latest eligible message. This keeps the shared prefix available while the conversation grows.

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)

Caching conversation history and tool results

The model requests tools by name and supplies their arguments. Your application must execute them. execute_function is a helper you build to route those requests to the matching Python functions and return their results. The following loop uses this helper to run each requested tool. It appends the model’s output and all tool results to the history list, then sends that list again until the model returns a final answer. In implicit mode, Amazon Bedrock can reuse the unchanged prefix and cache the added content through the last tool result. Keep earlier messages and the initial TOOLS unchanged.

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)

For follow-ups, append a new user message and call the helper again. Keep earlier messages unchanged so their cached endpoints remain available.

Rolling breakpoints: Each turn reads everything before it

Rolling breakpoints, where each turn reads all earlier turns and writes only its own tool result.

Figure 3: Rolling breakpoints, where each turn reads everything before it and writes only its own tool result

For a separate explicit-only run, recreate the two-message history from the combined Hello cached world example. This loop wraps tool results in input_text blocks and marks the final result in each batch. Earlier markers stay in history. See the current lookup rules for long conversations.

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 tied for the best implicit result in this six-turn workload: about 62 percent lower input cost than running without caching. Explicit rolling breakpoints saved about 61 percent on the same model. Both reused more history as the conversation grew. These are workload results. Measure your own agent’s savings.

Bar chart comparing the input cost of each caching setup on GPT-6 Astra, relative to no caching.

Figure 4: Input cost of every caching setup on GPT-6 Astra, relative to no caching

Watch out for:

  • Only a static breakpoint in explicit-only mode – The tools and instructions are cached, but the growing history is processed again on every turn (0.61). This limitation doesn’t apply to the combined example, which also saves through eligible conversation endings.
  • Moving a single breakpoint can prevent cache reuse and cause repeated writes in explicit-only mode. Keep the previous turn’s breakpoint too.
  • Only an implicit write through a changing task – A new task might share the tools and instructions but not the previously saved endpoint. Add an explicit marker after the shared instructions if different tasks need to reuse that shorter prefix.

When tools change

Sometimes an agent’s tool list changes, or new tools get added. Keep the initial list stable: adding, removing, or reordering definitions can invalidate the cached prefix. Choose the approach that fits how the tool becomes available.

When known tools are invoked in different steps of the agent’s flow: Send the same sorted TOOLS and use allowed_tools to limit what the model can call. With allowed_tools, "mode": "auto" lets the model decide whether to call an allowed tool, and "mode": "required" requires at least one allowed tool call. Pass this as tool_choice. The restriction doesn’t change the cached catalog:

tool_choice={"type": "allowed_tools", "mode": "auto",
             "tools": [{"type": "function", "name": "get_owner"}]}

New tools during a session. Both Amazon Bedrock Runtime and Amazon Bedrock Mantle endpoints accept an input item type called additional_tools for GPT-6 Astra, GPT-6.1 Sol, and GPT-5.6 Luna. For model availability by AWS Region, see Supported models by AWS Region in Amazon Bedrock. Register the new tool get_due_date in the helper function execute_function, then append its schema to the completed history from the agent loop. Keep the initial TOOLS unchanged:

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

A new tool arrives mid-session

Native additional_tools loading preserves the earlier cached prefix. The GPT-6 Astra Runtime test reads 2,068 tokens and writes a 145-token extension. Editing the initial catalog causes a cache miss.

Figure 5: Appending a tool keeps the earlier prefix reusable, with example counts from GPT-6 Astra on Amazon Bedrock Runtime

Continue the tool-execution loop with the new response. The loop adds the model’s output to history, runs any requested tools, and appends their results. Keep the additional_tools item in later requests so the new tool remains available. In implicit mode, Amazon Bedrock can cache through the new user message, including the tool definitions that precede it. In explicit-only mode, retain earlier breakpoints and add one to an input_text block in that user message. The additional_tools item itself can’t hold a breakpoint.

Different roles, different catalogs. When different agent roles need different tools, choose a stable tool profile, such as support or billing, at the start of each session. Each profile gets its own cached prefix. Mark the shared instructions so later tasks with that profile can reuse them, then use either mode as the conversation grows.

Cached tool profiles

Support and billing sessions each reuse the cache for their fixed tool profile.

Figure 6: Each tool profile has its own cached prefix, reusable by matching sessions

Subagents and side calls

Delegating shouldn’t make every helper reread the same background. Share the prefix, then add each helper’s task at the end. Be aware that when you use caching with AI agents, tools and system prompts can be leaked or exploited. Establish responsible AI practices that include your prompt caching implementation as part of that governance.

Warm once, then fan out. Send an ordinary request first and wait for it to finish before launching helpers. Mark the shared context explicitly in every request, even when the mode is implicit:

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))

The helpers use the same TOOLS, model, and KEY as the warm-up. A warm-up that ends at the output cap can still populate the cache. The usage check confirms cache activity before the helpers start.

Subagents: Warm the shared prefix once, then fan out

An ordinary warm-up request populates the shared prefix before four subagents start.

Figure 7: Warm the shared prefix before launching helpers, and each helper then adds its own task

Give side calls the same beginning. The agent loop builds history by appending every resp.output and its matching tool results, including encrypted reasoning. After the parent finishes, copy that complete list for a summarizer or judge. The same helper keeps the tools, model, and key unchanged:

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.

The helper can reuse the cached portion of history, but the appended developer message doesn’t automatically extend the cache to include the summary instruction. If later requests reuse the same history and instruction, place the instruction in an input_text block with prompt_cache_breakpoint. For a one-time summary, no additional marker is needed.

Pauses, restarts, and the cache key

Pauses. Cached prefixes have a minimum 30-minute time to live (TTL), refreshed by successful reuse, and might remain longer. If a session resumes soon, an ordinary warm-up can refresh the prefix before it expires. Compare that request’s cost with letting the cache expire. For the TTL rules, see the Amazon Bedrock documentation.

New sessions. Keep prompt_cache_key stable for requests that are meant to share a prefix. A session-specific key isolates sessions even when their prompts match. In the following example, switching to a shared key let sessions reuse the prefix.

New sessions reuse the cache only if they share the key

The original experiment compares per-session cache keys with a stable shared key.

Figure 8: A session-specific key separates cache entries, and a shared key lets matching sessions reuse them

Reuse of existing cache content requires the same model, AWS account, and Region, and an available matching prefix. The following example combines an explicit startup marker with implicit mode. Each session starts with the same instructions and its own task. Both use the same computed key:

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)

On a cold start, session 1 writes the shared prefix. Session 2 can read it even though its task can be different. Each task can also be cached implicitly. Check the usage counters: matching keys and markers make reuse possible, but a cache entry still has to be available.

What prompt caching delivers

Cost. For a GPT-6 Astra powered agent with an 8,000-token startup prefix and six turns adding 2,000 tokens each, one-time static caching reduced cost to $0.50, and rolling breakpoints reduced modeled input cost by 64 percent: from $0.87 to $0.32 per session.

Line chart of cumulative input cost per session for the support-agent example on GPT-6 Astra.

Figure 9: Cumulative input cost per session for the support-agent example, using Amazon Bedrock on-demand prices for GPT-6 Astra

How to tell caching is working

Start with response usage. cached_tokens reports input read from the cache, and cache_write_tokens reports input written to it. As the conversation grows, check that later turns reuse earlier history and that new writes reflect the content being added.

For production monitoring on the Amazon Bedrock Runtime endpoint, use the CacheReadInputTokenCount and CacheWriteInputTokenCount metrics in Amazon CloudWatch. Enable model invocation logging when you need individual request records, then query them with Amazon CloudWatch Logs Insights. For the Amazon Bedrock Mantle endpoint, record the response usage fields in your application logs. Its Amazon CloudWatch namespace currently has no separate cache counters.

To check costs, filter AWS Cost Explorer to Amazon Bedrock and group by Usage type. Cache reads, cache writes, and uncached input appear as separate entries per model. Exclude output-token entries when calculating their shares of input cost.

A short checklist

  • Keep the reusable prefix stable. Put shared tools and instructions first, then append conversation history and new turns. Changing a timestamp, message, or tool definition prevents reuse beyond that change. A shorter prefix can still be reused if it has its own saved endpoint.
  • Choose boundaries around content you want to reuse. Use implicit caching for growing history, explicit markers for shared prefixes, or both. Explicit-only mode avoids writing disposable suffixes. When extending an explicit-only conversation, retain earlier markers so later requests can find the saved prefixes.
  • Keep the initial tool catalog unchanged. Use allowed_tools to restrict which tools are callable. Append known definitions with additional_tools, or discover them through namespaced client-side tool_search. Choose a stable tool profile when different agent roles need different catalogs.
  • Check prefix size and lifetime. A cacheable prefix must contain at least 1,024 tokens. Cached prefixes remain eligible for reuse for at least 30 minutes after their latest write or successful reuse.
  • Warm shared content before launching helpers. Mark the shared prefix in both the warm-up and helper requests, including when using implicit mode. Finish the warm-up request before starting parallel calls.
  • Keep cache keys stable within each sharing group. Requests intended to reuse a prefix must use the same key. Separate keys can keep cache accounting distinct between customers or users.
  • Measure savings across the workflow. Cache writes cost 1.25 times the normal input rate. Read prices depend on the model. Track write costs alongside read savings, using response usage during development and monitoring and billing data in production.

Waking up on February 3rd

Bill Murray only escapes Groundhog Day once he stops repeating the same mistakes and starts carrying what he learned from one day to the next. Agents get there in a similar, if less romantic, way: by keeping the start of their prompt byte-for-byte stable and using automatic or explicit cache boundaries as the history grows.

In this post, we covered how to cache shared tools and instructions, grow the cache across agent turns, and preserve reuse when adding tools, launching helpers, or starting new sessions. Keep the shared prefix stable and append new information as the workflow progresses. Use implicit caching for growing conversations and explicit breakpoints for shared content. Combine them when you need both, or choose explicit-only mode when a changing suffix is unlikely to be reused. Compare cache reads, writes, total input cost, and latency to find what works best for your agent. To apply these patterns, follow the Amazon Bedrock prompt caching documentation for OpenAI models and check model availability by AWS Region and endpoint. Start with one workflow and measure the savings as your conversations grow.


About the author

原文版权归 AWS Machine Learning 及原作者所有,本站转载仅供阅读。如需删除,请联系 support@aisa.one。