当应用反复把同一套系统指令、产品文档、工具定义或租户配置发送给基础模型时,真正浪费的往往不是模型输出,而是重复计算的输入 Token。Amazon Bedrock 的 Prompt Caching 可以复用这类稳定上下文,在合适的场景下将输入 Token 成本最高降低 90%,同时减少首字节延迟。
本文围绕 Converse API,拆解六类常见用法:缓存消息内容、系统提示词、工具定义、混合 TTL、租户隔离,以及在 LangChain 中接入缓存。示例中的模型 ID 和区域需要替换成你账户中已启用 Prompt Caching 的模型。
先判断:什么内容值得缓存
Prompt Caching 适合“内容重复率高、有效期内会被多次使用”的输入。例如:
- 每轮对话都要加载的系统提示词;
- 一份不会频繁变化的产品手册或代码仓库摘要;
- 多次调用中完全一致的工具定义;
- 同一个租户的固定业务规则;
- 批处理任务中反复使用的长文档和固定指令。
它不适合每次都变化的短输入,也不能把用户 A 的私有上下文缓存后直接用于用户 B。缓存命中必须建立在上下文一致和隔离边界正确的基础上。
一个实用原则是:把稳定内容放在前面,把请求相关内容放在后面,并在稳定内容结束的位置放置缓存点。这样,后续请求只需要传递变化的尾部内容。
Converse API 中的基本缓存方式
在 Converse API 中,可以在可缓存的消息内容、系统提示词或工具定义后插入 cachePoint。下面是一个可直接改造的 Python 示例。
运行前请完成 AWS 凭证配置,例如使用 aws configure、环境变量或 IAM Role,并将 MODEL_ID 替换为目标模型。
import json
import boto3
REGION = "us-east-1"
MODEL_ID = "your-prompt-caching-enabled-model-id"
client = boto3.client("bedrock-runtime", region_name=REGION)
stable_context = """
你是一个电商客服助手。
回答必须遵守以下规则:
1. 不编造库存、价格和物流状态。
2. 无法确认时明确说明需要人工核验。
3. 优先使用简洁的中文回答。
""".strip()
response = client.converse(
modelId=MODEL_ID,
system=[
{
"text": stable_context,
},
{
"cachePoint": {
"type": "default"
}
}
],
messages=[
{
"role": "user",
"content": [
{
"text": "订单 10001 为什么还没有发货?"
}
]
}
],
inferenceConfig={
"maxTokens": 300,
"temperature": 0.2
}
)
print(response["output"]["message"]["content"][0]["text"])
print(json.dumps(response.get("usage", {}), indent=2, ensure_ascii=False))
cachePoint 的具体可用能力取决于模型、区域和当前 SDK/API 版本。部署前应确认目标模型支持 Prompt Caching,并使用与服务端能力匹配的 boto3 版本。响应中的 usage 字段可以帮助观察缓存写入和读取情况;字段名称可能随 SDK 版本有所差异,建议在实际环境中记录完整的 usage 对象。
六种值得落地的场景
1. 缓存重复的消息内容
如果应用会反复让模型阅读同一份手册、代码片段或长文档,可以把这段内容放在消息前部,并在其后设置缓存点。每次请求只追加新的问题。
messages = [
{
"role": "user",
"content": [
{"text": "以下是产品使用手册:\n" + product_manual},
{"cachePoint": {"type": "default"}},
{"text": "用户的新问题:如何申请退款?"}
]
}
]
这里的关键不是把所有内容都放进缓存,而是让产品手册在多次调用之间保持字节级或语义要求上的一致。若每次都在手册前面插入当前时间、随机 ID 或不同的包装文本,命中率可能下降。
2. 缓存系统提示词
系统提示词通常包含角色、输出格式、安全规则和业务边界,是最稳定的一类上下文。将缓存点放在系统提示词末尾,可以让同一应用的连续请求复用它。
适合缓存的内容包括:
- 固定的客服角色和语气;
- JSON 输出格式;
- 统一的安全和合规规则;
- 长篇的领域术语表。
会随请求变化的用户身份、当前权限和临时策略,不应无条件放入共享缓存。若这些信息属于租户或用户私有数据,应采用隔离的缓存键或独立的会话边界。
3. 缓存工具定义
Agent 应用每轮都可能发送相同的工具 schema。工具数量多、参数描述长时,工具定义本身就会占用大量输入 Token。
tools = [
{
"toolSpec": {
"name": "get_order_status",
"description": "查询订单的当前状态。只能查询当前用户有权访问的订单。",
"inputSchema": {
"json": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单编号"
}
},
"required": ["order_id"]
}
}
}
},
{
"cachePoint": {
"type": "default"
}
}
]
response = client.converse(
modelId=MODEL_ID,
system=[{"text": "你是订单助手。"}],
toolConfig={"tools": tools},
messages=[
{
"role": "user",
"content": [{"text": "帮我查询订单 10001。"}]
}
]
)
工具执行结果通常是动态内容,不应与稳定的工具定义混在同一个缓存区。把 schema 放在前面、工具调用结果放在后面,能更清楚地区分可复用和每次变化的部分。
4. 混合 TTL:不同内容使用不同生命周期
并不是所有稳定内容都需要同一个缓存时长。可以把长期不变的公共规则设置为较长 TTL,把频繁更新的业务配置设置为较短 TTL。实际 TTL 选项、字段名称和可缓存边界需要以目标模型及当前 Bedrock API 文档为准;部分模型或 SDK 版本可能只暴露默认 TTL。
可以按下面的思路设计:
共享系统规则 -> 较长 TTL
版本化产品手册 -> 较短 TTL
当前用户问题 -> 不缓存
实时库存和价格 -> 不缓存
混合 TTL 的价值在于避免“为了缓存一小段长期内容,却被短生命周期数据频繁刷新”的情况。若当前 API 版本不支持在同一请求中配置不同 TTL,可以拆分上下文,或者只缓存收益最高、最稳定的那一段。
5. 租户隔离
多租户 SaaS 场景需要特别谨慎。租户 A 的业务规则、合同条款、内部文档和历史摘要不能因为文本相似而被租户 B 复用。
可以这样设计隔离边界:
tenant_id
-> 独立的会话/上下文构建器
-> 独立的缓存内容版本
-> 独立的权限校验
-> 只允许同一租户复用缓存
不要把 tenant_id 仅当作应用日志字段,而应把它纳入缓存管理和上下文构建逻辑。租户配置发生变化时,使用版本号或内容哈希让旧上下文自然失效。例如,将 tenant_id + policy_version + document_version 作为应用侧的上下文版本标识。
6. 在 LangChain 中接入
如果应用通过 LangChain 统一模型调用,可以在底层 Bedrock Converse 请求中保留缓存点。具体配置方式会受 LangChain、langchain-aws 和 boto3 版本影响,因此建议先确认当前集成是否原样透传 system、toolConfig 和 cachePoint。
一个可改造的初始化示意如下:
from langchain_aws import ChatBedrockConverse
llm = ChatBedrockConverse(
model="your-prompt-caching-enabled-model-id",
region_name="us-east-1",
max_tokens=300,
temperature=0.2,
)
# 将稳定系统提示词和 cachePoint 放入底层 Converse 请求支持的消息结构。
# 不同 LangChain 版本的消息 metadata/extra_body 参数不同,
# 请在项目版本中确认 cachePoint 的透传方式。
result = llm.invoke([
("system", "你是一个遵守固定业务规则的客服助手。"),
("human", "订单 10001 为什么还没有发货?")
])
print(result.content)
如果封装层无法透传缓存点,直接使用 boto3 Converse API 往往更容易验证命中效果。先用原生 API 建立基准,再决定是否把能力封装回 LangChain,是比一开始就排查多层抽象更省时间的路径。
成本和延迟如何验证
不要只看单次调用价格。Prompt Caching 的收益来自重复请求,因此应使用接近生产的请求序列进行测试:
- 首次请求写入缓存;
- 在 TTL 内发送多次相同前缀、不同问题的请求;
- 观察 usage 中的缓存写入和读取 Token;
- 记录首字节延迟、总延迟和每次输入成本;
- 等待缓存过期后再次测试冷启动。
可以为每次请求记录以下字段:
model_id
region
tenant_id_hash
context_version
input_tokens
cache_write_tokens
cache_read_tokens
output_tokens
latency_ms
request_id
最终的节省比例取决于缓存前缀长度、命中率、模型定价、缓存写入成本和请求频率。不要直接把“最高可降低 90%”当作所有业务的实际结果;短提示词、低重复率或频繁失效的上下文,收益可能很有限。
上线前检查清单
- 确认目标模型和区域支持 Prompt Caching;
- 升级并锁定 boto3、LangChain 等 SDK 版本;
- 把稳定前缀放在前面,动态问题放在后面;
- 为系统提示词、消息内容和工具定义分别评估缓存价值;
- 为租户、用户权限和上下文版本建立明确隔离;
- 通过 usage 和延迟指标验证真实命中,而不是只看请求成功;
- 为缓存未命中、TTL 过期和模型不支持设计降级路径;
- 避免把实时库存、价格、权限和个人隐私数据放入不必要的共享缓存。
Prompt Caching 最适合被当作“上下文工程”的一部分,而不是简单的开关。先找出重复率最高、Token 占比最大的前缀,再用 Converse API 小规模压测;当成本、延迟和数据隔离都得到验证后,再推广到工具调用、租户级知识库和 LangChain 应用中。