每次启动你的智能体时,它都必须在执行操作之前重新阅读自己的全部人生经历。以下介绍如何让它记住上下文,而无需再次向它提供相同的上下文。
在电影 《土拨鼠之日》中,Bill Murray饰演的角色每天都重新经历同一个2月2日。他在同一家旅馆、同一座小镇中听着收音机里播放的同一首歌醒来,如此日复一日,据一些影迷估计长达34年。
他前一天取得的任何成果都不会延续下去,除非他从头再来一遍。AI智能体就生活在《土拨鼠之日》的某个版本中。智能体每采取一步行动(调用工具、读取结果、决定下一步做什么),都必须向模型发送它迄今为止的完整经历以维持连续性:工具定义、指令、之前的每一次工具调用以及每一个结果。模型只根据你提供的输入预测下一个token。它在请求之间没有任何内部记忆,因此必须把所有内容重新读一遍。到了编码会话的第20轮,模型会重新阅读第1至19轮,而为这些token反复付费的人是你。
提示词缓存打破了这个循环。该服务会存储你的输入提示词(指令、工具和内容),并在下一次请求时复用它,而输入价格只需原价的一小部分。
在这篇文章中,我将带你了解一个智能体的一生,从它的第一轮到它获得新工具并派生辅助智能体的时刻。在此过程中,我会向你展示提示词缓存如何降低成本,并保持智能体的响应速度和一致性。
智能体向其LLM发送的内容
在讨论缓存之前,先看看典型智能体对大语言模型(LLM)的首次调用包含什么会有所帮助:
- 工具列表 – 智能体可调用的每个函数的定义。发布新版本时会发生变化。
- 指令 – 开发者提示词:规则、人设、策略、示例。每次发布也会变化。
- 历史记录 – 本次会话中迄今为止发生的一切:任务、模型的工具调用以及工具结果(每一轮都在增长)。
- 新轮次 – 最新的工具结果或用户消息(每次都可能不同)。
如果不设置缓存,智能体在每一轮中向模型发送的内容:
图1:智能体在每一轮中向模型发送的内容
提示词缓存在底层的工作原理
当模型读取一个提示词时,它首先会进行一次称为 prefill(预填充)的写入操作。在预填充期间,它会处理每个输入token,并构建其key和value向量(也称为key-value或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 才符合条件。
缓存静态内容时,请使用显式(explicit)模式:自行选择已保存前缀的结束位置。
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)放在已缓存指令之后。
随着内容不断增长且变得多样化(不同的工具和内容),使用隐式模式让服务跟随对话。在这里,第一个请求没有显式标记。其最新符合条件able的消息是用户的任务,因此自动断点位于该任务之后,并将工具、指令和任务一并覆盖。为清晰起见,下面的示例中显式展示了该模式,尽管隐式是默认值。Setting 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。请求级模式保持隐式。块级标记在指令之后添加一个显式保存点。服务也可以通过最新符合条件able的消息进行保存。这样在对话增长时,共享前缀仍然可用。
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 区域划分的受支持模型。在辅助函数 get_due_date 中注册新工具 execute_function,然后将其架构附加到来自代理循环的已完成历史记录之后。保持初始的 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:在启动辅助代理之前预热共享前缀,然后每个辅助代理添加自己的任务
为辅助调用提供相同的开头。 代理循环通过追加每个 history 及其匹配的工具结果(包括加密的推理)来构建 resp.output 。在父代理完成后,将该完整列表复制给摘要器或评判器。同一个辅助代理保持工具、模型和密钥不变:
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的已缓存部分,但附加的开发者消息不会自动扩展缓存以包含摘要指令。如果后续请求复用相同的历史和指令,请将该指令放入一个 input_text 块中并使用 prompt_cache_breakpoint。对于一次性的摘要,不需要额外的标记。
暂停、重启与缓存键
暂停。 已缓存前缀的最短生存时间(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 倍。读取价格取决于模型。使用开发期间的响应使用情况以及生产环境中的监控和账单数据,将写入成本与读取节省一同跟踪。
2月3日醒来
比尔·默瑞只有在不再重复同样的错误、并开始把一天中学到的东西带到下一天时,才能逃离《土拨鼠之日》。代理以类似(尽管没那么浪漫)的方式实现这一点:保持其提示词开头逐字节稳定,并随着历史增长使用自动或显式缓存边界。
在这篇文章中,我们介绍了如何缓存共享工具和指令、跨代理回合扩展缓存,以及在添加工具、启动辅助程序或启动新会话时保持复用。保持共享前缀稳定,并随着工作流程的推进追加新信息。对于不断增长的对话使用隐式缓存,对共享内容使用显式断点。当两者都需要时可以结合使用,或者在更改的后缀不太可能被复用时选择仅显式模式。比较缓存读取、写入、总输入成本和延迟,找出最适合您的代理的方案。要应用这些模式,请遵循 Amazon Bedrock 面向 OpenAI 模型的提示词缓存文档 ,并查看 按 AWS 区域和端点划分的模型可用性。从一个工作流程开始,随着对话的增长衡量节省情况。