🎁 新人 免费注册,送 10 次调用,最高 $1,免绑卡。
LangChain 提示词缓存:真正能命中缓存的配置方式

LangChain 提示词缓存:真正能命中缓存的配置方式

目录
  1. 先确认一下:你要找的是哪种“缓存”?
  2. 解决办法:使用内容块,而不是字符串
  3. 模板变量放在哪里,决定缓存命中率
  4. 工具定义也会被缓存
  5. 多轮对话:把标记移到最后一条消息
  6. 正确读取指标,并认清字段名
  7. 隐式缓存:顺序错误时不会报错,更需要重点监控
  8. 检查清单
  9. 免责声明
  10. 来源

下面这段 LangChain system prompt 看起来完全没问题,实际上什么也缓存不了:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", BIG_STABLE_SYSTEM_PROMPT),   # the syntax every tutorial uses
    ("human", "{question}"),
])

我们用完全相同、包含 1,800 个 token 的 system prompt 连续请求了两次 claude-sonnet-5,然后查看 usage 字段。两次调用的缓存写入和读取都是 0。不是部分命中,也不是缓存被切碎,而是根本没有缓存。原因是 Anthropic 只缓存使用 cache_control 标记的内容,而 ("system", ...) tuple 中的普通字符串没有地方放这个标记。LangChain 最方便的语法,恰好会让你错过全部折扣,而且不会报错。

TL;DR

  • LangChain 的 ("system", "string") tuple 无法携带 cache_control,所以 Claude 不会缓存任何内容:实测在 claude-sonnet-5 上重复发送相同的 1,800-token system prompt,缓存写入和读取均为 0。
  • 解决办法是使用带有内容块级 cache_controlSystemMessage;system block 上的一个标记也能覆盖通过 bind_tools 绑定的工具。
  • 使用 langchain-anthropic 1.4.8 时,即使确实发生了缓存写入,input_token_details.cache_creation 仍然是 0;真正的计数位于 ephemeral_5m_input_tokens
  • RAG prompt 顺序错误时,如果把变化的上下文放在固定规则之前,每次调用都要支付约 1.25 倍的缓存写入溢价,甚至比完全不用缓存还贵。

系列文章:第 5 篇,共 5 篇 · 前文:第 1 篇——缓存原理 · 第 2 篇——服务商对比与评估 · 第 3 篇——可运行代码教程 · 第 4 篇——不同场景下的最佳 LLM

这是缓存系列的第 5 篇。第 1 篇介绍前缀缓存的工作原理,第 3 篇提供原生 SDK 教程,提示词缓存完整指南则横向覆盖各服务商。本文关注 LangChain 代你组装 prompt 后会发生什么。以下数据均于 2026-07-04 通过 Synthorai gateway 实测,使用 langchain-core 1.4.8、langchain-anthropic 1.4.8 和 langchain-openai 1.3.3。

先确认一下:你要找的是哪种“缓存”?

这两个毫不相关的功能都叫缓存,而搜索时最容易进入的 LangChain 文档页面,通常讲的不是你需要的那一种。

响应缓存(LangChain 的 InMemoryCache提示词缓存(本系列主题)
存储内容整个补全结果,保存在应用中prompt 前缀的 KV 状态,保存在服务商端
何时省钱完全相同的请求再次出现不同请求共享同一前缀
配置位置set_llm_cache(InMemoryCache())、SQLite、Rediscache_control 标记或自动前缀匹配
Agent 循环、RAG、聊天几乎无用(每次请求都不同)主要优化手段,因为每轮都会重复发送 system prompt 和工具

这里的“完全相同”是严格意义上的完全一致:内置缓存使用(序列化 prompt、模型配置字符串)这一对值作为 key。实测中,完全相同的请求在 0 ms 内直接返回,没有发起 API 调用;prompt 多一个空格就会 miss;同一个 prompt 只把 max_tokens 改 1,也会 miss。(缓存重放还会返回原调用的 usage 数值,因此简单累加 token 会导致重复计数。)第三方集成中有语义缓存,但内置缓存只支持精确匹配。

因此,set_llm_cache 适合在测试中对完全相同的调用去重;至于 agent 每轮都要重新发送的 2,000-token system prompt,则应该交给提示词缓存,而且必须用正确的方式组装 prompt。

解决办法:使用内容块,而不是字符串

cache_control 位于内容块内部,因此 system message 必须使用内容为 block 的 SystemMessage,不能是裸字符串:

from langchain_anthropic import ChatAnthropic
from langchain_core.messages import SystemMessage
from langchain_core.prompts import ChatPromptTemplate

llm = ChatAnthropic(
    model="claude-sonnet-5",
    base_url="https://synthorai.io",   # any Anthropic-compatible endpoint
)

prompt = ChatPromptTemplate.from_messages([
    SystemMessage(content=[{
        "type": "text",
        "text": BIG_STABLE_SYSTEM_PROMPT,
        "cache_control": {"type": "ephemeral"},   # a bare string has nowhere to put this
    }]),
    ("human", "{question}"),
])
chain = prompt | llm

通过同一 gateway 测试相同的 1,800-token system prompt:

调用字符串 tuple 语法内容块语法
第 1 次(冷缓存)写入 0 / 读取 0写入 1,875 / 读取 0
第 2 次,不同问题写入 0 / 读取 0写入 0 / 读取 1,875

热缓存读取的计费大约是输入价格的 10%。在 Claude 上,仅调整这一处结构,结果就从长期按全价付费,变成每次调用中固定部分都享受 90% 的折扣。具体成本计算见第 1 篇;标记机制则与 LangChain Anthropic 集成文档Anthropic 提示词缓存指南中的原生 SDK 用法一致。

模板变量放在哪里,决定缓存命中率

LangChain template 可以轻松地在任意位置插入变量,这也正是风险所在。缓存 key 是字节级完全一致的前缀。我们把日期放进缓存块中进行测试:

SystemMessage(content=[{
    "type": "text",
    "text": f"Today is {today}. " + BIG_STABLE_SYSTEM_PROMPT,   # variable INSIDE the block
    "cache_control": {"type": "ephemeral"},
}])
调用结果
日期 A,问题 1写入 1,865(该值首次出现,冷缓存)
日期 A,问题 2读取 1,865(值相同,命中)
日期 B,问题 1写入 1,865(新值,再次冷缓存)

缓存没有失效,只是变量也进入了缓存 key。像日期这种会重复出现的值,每个值只需写入一次缓存,之后都能命中。时间戳或 request ID 这类每次调用都不同的值,会让每次请求都成为冷写入,命中率始终为零。

现实中代价最高的类似错误出现在 RAG 中。很多 chain 会把检索到的上下文放在 system prompt 顶部,位于固定指令之前。我们分别测试了两种顺序,每次查询都会更换 800-token 的检索上下文,并对 system block 加标记:

prompt 内部顺序调用 1调用 2(新查询、新上下文)
上下文在前,规则在后写入 3,133再次写入 3,133,读取 0
规则在前(已标记),上下文放在 human turn写入 1,852读取 1,852

错误顺序不只是“没有折扣”:每次调用都要为全部 3,133 个 token 支付缓存写入溢价,约为正常输入价格的 1.25 倍,而且永远不会读取缓存。启用缓存但顺序错误的 RAG prompt,比完全不缓存还贵。 固定内容位于变化内容之后,因此实际上起不到任何作用。

实测可以归纳出以下规则:

  • 静态文本放在最前面,并置于已标记的 block 内。 包括 system 规则、工具定义和 few-shot 示例。
  • 所有变化内容都放在标记之后,最好放进 human turn:检索上下文、日期、用户问题。
  • 只有当某个变量重复频率足以摊薄自身缓存写入成本时,才适合放在 block 内。

工具定义也会被缓存

Agent 每次调用都会重新发送工具 schema。在 Anthropic 的请求结构中,工具位于 system prompt 之前。标记的含义是“缓存从请求开头到此位置的全部内容”,因此需要确认两个实际问题:system block 上的标记是否也会覆盖它前面的工具?LangChain 的 bind_tools 是否能在每次调用中生成完全相同的字节?如果序列化结果有波动,前缀就会变化,每次调用都会 miss。

两个问题的实测答案都是肯定的。使用同一个带标记的 system prompt 时,不绑定工具的热缓存读取量为 1,861 个 token,绑定两个工具后则为 2,389 个 token:多出的 528 个 token 正是从缓存读取的工具 schema。连续三次调用都精确读取 2,389 个 token,说明 bind_tools 每次都能生成相同的序列化结果,framework 不会给前缀引入噪声。明确来说:只要 system block 带有标记,工具本身就不需要 cache_control;位于工具之后的这一个标记已经足够。

另一种配置适用于特定情况:工具是请求中最大的固定内容,而 system prompt 很短或者不存在。这时请求中仍然需要一个标记,可以把它放在工具上。由于 @tool 装饰的函数没有字段可以承载标记,因此这种方式只能使用原生 Anthropic 格式的 dict;bind_tools 会原样透传该 dict:

# variant: NO marked system block anywhere; the tool carries the request's only marker
llm.bind_tools([{
    "name": "get_weather",
    "description": LONG_TOOL_DESCRIPTION,
    "input_schema": {...},
    "cache_control": {"type": "ephemeral"},   # passes through bind_tools verbatim
}])

实测结果:请求中没有带标记的 system message,冷缓存写入 3,002,热缓存读取 3,002。

多轮对话:把标记移到最后一条消息

对话看起来也是一个顺序问题,实际情况正好相反:对话历史只会不断追加,所以顺序天然正确,整个 transcript 都是固定前缀。这里的问题在于覆盖范围。system block 上的标记只缓存 system block,不会缓存后面的内容。随着历史不断增长,热缓存读取量始终等于 system 的大小,所有累积的 turn 都按普通输入计费。

解决方式与原生 SDK 相同:把标记放在最新一条消息上,让断点不断向后移动,把截至当前的整段对话变成缓存前缀:

def marked(text):
    return HumanMessage(content=[{
        "type": "text", "text": text,
        "cache_control": {"type": "ephemeral"},
    }])

# each turn: history stays plain, only the newest human message carries the marker
llm.invoke([system, *history, marked(new_question)])

两轮实测结果:第 1 轮写入 1,864;第 2 轮读取 1,864,只写入新增的 15 个 token(上一轮回答加新问题),之前的前缀按约 10% 的读取价格计费。这正是 agent 循环需要的模式,而 LangChain 只需使用普通 message list 即可实现。Anthropic 每个请求最多允许四个标记,因此滑动标记可以与 system block 或工具上的固定标记组合使用。

正确读取指标,并认清字段名

LangChain 会把 usage 标准化为 usage_metadata,但这里有个容易踩的坑:在我们的所有测试中,使用 langchain-anthropic 1.4.8 时,即使实际发生了缓存写入,标准字段 input_token_details.cache_creation 仍然保持为 0。真实写入量位于一个非标准 key 中:

r = chain.invoke({"question": "..."})
det = r.usage_metadata["input_token_details"]
det["cache_read"]                  # correct on hits (1875 above)
det["cache_creation"]              # 0 even on a cold write; do not alert on this
det["ephemeral_5m_input_tokens"]   # the actual write count (1875)

服务商正确报告了写入量(原始响应中的 cache_creation_input_tokens: 1875,可通过 r.response_metadata["usage"] 查看);只是标准化映射把它放到了 TTL bucket 对应的 key 中。如果成本 dashboard 监控的是 cache_creation,它会显示缓存写入没有成本,而写入溢价其实一直在累积。要么以原始 usage 对象为准,要么明确了解各 bucket key。gateway 错报缓存字段也属于同类问题,详见你的 LLM Gateway 会谎报缓存数据吗?

隐式缓存:顺序错误时不会报错,更需要重点监控

Claude 使用显式缓存。GPT 和大多数开源权重模型会根据前缀匹配自动缓存,不需要标记。通过 LangChain 使用时,只需更换 constructor,同一条 chain 就能工作:

llm = ChatOpenAI(model="glm-5.2", base_url="https://synthorai.io/v1")

使用普通字符串 system prompt,不加标记:GLM 5.2 第二次调用从约 1,850-token 的前缀中读取了 1,088 个 token。(不是全部:自动缓存通常按较粗的 block 粒度匹配,而不是逐字节匹配到结尾。例如 OpenAI 文档注明其粒度为 128 个 token。)目前看起来相当于白捡折扣。但前面 RAG 表格中的顺序风险在这里完全一样,而且失败方式更隐蔽。我们在自动缓存路径上重新进行了同样的顺序实验,每次调用都更换检索上下文:

顺序(无标记,自动缓存)调用 1调用 2(新查询、新上下文)
上下文在前,规则在后读取 0读取 0
规则在前,上下文放在 human turn读取 0读取 1,088

顺序错误时必然是零命中:变化的上下文位于开头,任意两次调用都没有共同前缀,因此永远拿不到折扣。在显式缓存路径上,同样的错误至少会在账单中体现为每次调用的缓存写入溢价;在隐式缓存路径上,没有溢价、没有错误,也没有任何提示。prompt 只是悄悄地始终不符合缓存条件,而你可能还以为“自动”就等于“已经生效”。由于没有标记可放,prompt 顺序是隐式缓存路径唯一可调的参数。

因此,要通过指标验证生产环境中的实际命中情况,而不是只在测试中看一次:LangChain 查看 input_token_details.cache_read,原始响应查看 prompt_tokens_details.cached_tokensOpenAI 自动缓存文档还规定了至少 1,024-token 的前缀要求;不同服务商的 TTL 和适用条件也不相同,这些内容见第 2 篇

检查清单

  • 在 Claude 上,("system", "...") 字符串 tuple 没有位置放置 cache_control:不会缓存,也不会警告。可缓存的 system prompt 应使用带有内容块和标记的 SystemMessage
  • 缓存 key 是字节级完全一致的前缀:静态内容放在前面,变量放在标记之后或 human turn 中。把 RAG 上下文放在规则之前,不仅会 miss,还会让每次调用都支付写入溢价。
  • 缓存块内的变量会让每个值分别生成一个缓存项:重复值可以摊薄成本;每次调用都唯一的值(时间戳、request ID)永远无法命中。
  • 工具位于前缀中 system prompt 之前,因此 system 标记也会缓存绑定的工具(bind_tools 的序列化是确定性的)。如果工具是最大的固定 block,可以改为在 Anthropic 格式的工具 dict 上放标记。
  • 在对话中,如果标记固定在 system block 上,不断增长的历史仍会按全价计费;应把标记放到最新消息上,让每轮读取之前的前缀,只写入增量。
  • 不要监控 input_token_details.cache_creation:即使发生写入,它仍然是 0,因此 dashboard 会显示缓存写入免费,而实际溢价一直在累积。真实计数位于 ephemeral_5m_input_tokens,也可以读取原始 response_metadata["usage"]
  • 对自动缓存模型(GPT、GLM、DeepSeek)来说,prompt 顺序是唯一可调参数,而且顺序错误时不会报错:没有溢价,没有错误,只是永远没有折扣。应通过 usage 字段验证命中情况。
  • set_llm_cache 按精确匹配的 prompt 和模型配置存储完整响应;只有相同请求重复出现时才有价值,不适用于 agent 循环。

要养成的习惯很简单:用内容块代替字符串,把静态内容放在变量之前,让标记随对话向后移动,并读取正确的 usage 字段。实测结果的差距是:每个固定 token 享受 90% 折扣,或者完全没有折扣;在 RAG 顺序错误时,甚至还要额外付费。LangChain 并不妨碍提示词缓存,只是它让错误结构和正确结构写起来同样容易。


免责声明

测试于 2026-07-04 通过 https://synthorai.io/ 完成,使用 langchain-core 1.4.8、langchain-anthropic 1.4.8、langchain-openai 1.3.3,模型为 claude-sonnet-5glm-5.2。测试使用约 1,800-token 的英文 system 前缀,样本量较小;连续调用间隔 1–2 秒,以便缓存写入完成。每组实验都使用全新生成的随机前缀,确保初始缓存为冷状态,因此不同表格中的基准 token 数量略有差异(1,852 到 1,875)。不同版本的库字段映射和服务商缓存行为可能变化;依赖这些数据之前,请在自己的技术栈上重新测试。

来源

← 返回博客