用 Amazon Bedrock Prompt Caching 降低重复上下文的成本与延迟

2026-09-16 23 预计阅读时间: 1 分钟
来源: aws.amazon.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:13 分钟

当应用反复把同一套系统指令、产品文档、工具定义或租户配置发送给基础模型时,真正浪费的往往不是模型输出,而是重复计算的输入 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 版本影响,因此建议先确认当前集成是否原样透传 systemtoolConfigcachePoint

一个可改造的初始化示意如下:

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 的收益来自重复请求,因此应使用接近生产的请求序列进行测试:

  1. 首次请求写入缓存;
  2. 在 TTL 内发送多次相同前缀、不同问题的请求;
  3. 观察 usage 中的缓存写入和读取 Token;
  4. 记录首字节延迟、总延迟和每次输入成本;
  5. 等待缓存过期后再次测试冷启动。

可以为每次请求记录以下字段:

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 应用中。


相关推荐