Claude message threads 拆解:prompt cache 还在,但要手动开
Claude 在 2026 年 11 月 2 日给 Messages API 加了一个 thread 参数,叫 message threads:第一轮把完整对话、system 和 tools 发给服务端,之后每轮只发新消息加上一轮的 response id。它解决的是长 agent 循环里一个很具体的浪费:每一轮都把整段对话重新上传、重新校验、重新分词。官方给的数字是,在 500k 到 1M token 的请求里,首 token 延迟中位数下降约 35%,并且对多数 Claude Code 用户默认开启。
关于大家最关心的两个问题,先给结论。prompt caching 还在,而且没有任何变化;message threads 不会替你开缓存,官方文档的原话是 A thread does not cache your prompt for you.。要开就在 thread 的每个请求上自己加 cache_control,顶层字段或块级断点都可以,但 thread 请求最多 4 个断点,顶层那个算一个。
一句话判断:thread 省的是上传开销和首字延迟,不省 token 账单;想又快又便宜,得把 thread 和 prompt caching 叠起来用。
它是什么:服务端替你保管一段短期对话
message threads 是 Messages API 的一个 beta 功能,beta header 是 message-threads-2026-08-12。它有三个动作:
- create:按你现在的方式发完整对话,加上
system、tools,请求体里带"thread": {"type": "create"}。API 把整段对话连同 Claude 的回复,存到这次响应的id下面。 - continue:只发上一轮之后的新消息,带
"thread": {"type": "continue", "previous_message_id": "<上一轮 id>"}。system和tools不要再发,thread 保留 create 请求里的值。 - replay:thread 是 best-effort 的。过期或者服务端没存上,continue 会返回 404,这时用完整对话重新 create,再从新响应的
id继续。
它不是 Claude Managed Agents 里的 session threads,这两个概念官方专门加了 note 区分。message threads 只是 Messages API 的一个传输入口,不涉及会话编排、工具循环或者持久化。
数据怎么存的,文档写得很直白:对话、system、tools 以及固定字段的指纹都放在内存里,用服务端托管的密钥加密,从不写入持久存储,最后一次使用后不超过 1 小时删除。代价是这个功能 不属于 ZDR 覆盖范围,也不适用于开了 HIPAA readiness 的组织。它不会影响你对其他 ZDR 功能的正常使用,但这一段对话在 thread 生命周期内确实存在于服务端内存中。
另一个容易被忽略的前提:你的应用始终是对话的唯一可信来源。官方要求你自己保存每一条消息,并且从一开始就写好 replay 路径。create 和 continue 的响应都不会告诉你这一轮有没有被存下来,你只能在下一次 continue 时才知道。
为什么值得做:每轮重新上传的隐藏成本
不带 thread 的时候,第二轮请求要把第一轮的 user 消息、assistant 回复、工具调用、工具结果全部重新发一遍。API 收到之后要做完整校验和分词,然后才调用模型。对话越长,这部分不可见的开销越大,而且它会在每一轮重复。
带 thread 之后,这部分工作大部分只覆盖新增的那一轮。注意措辞:大部分,不是全部。Claude 仍然会读完整对话,所以输入 token 的计费和上下文窗口的占用跟不带 thread 完全一样。省下来的是网络上传、请求体解析、schema 校验和分词这些前置环节。
这解释了为什么官方只承诺 TTFT 下降,而没有承诺账单下降。也解释了为什么它把收益写死在「long agent loops」上:只有在轮次多、上下文长、每轮新增内容少的场景里,前置开销的占比才足够高。
从演进角度看,以前解决同一个问题的办法是在客户端裁剪上下文:早期摘要、只保留最近 N 轮、把工具结果压缩成一行。这些做法都在改内容,有信息损失,而且要自己维护一套摘要逻辑。thread 换了个方向:内容一条不动,只把重复传输的部分去掉。但它只保管很短的时间,所以它不是上下文管理方案,只是一个传输优化。
怎么用:create 和 continue 的最小请求
所有 thread 请求都要带 beta header:
anthropic-beta: message-threads-2026-08-12
第一轮,完整请求加一个 thread 字段:
{
"model": "claude-opus-5-5",
"max_tokens": 1024,
"cache_control": {"type": "ephemeral"},
"system": "You are a support agent for Acme. Keep replies short.",
"tools": [
{
"name": "get_order",
"description": "Look up an order by id",
"input_schema": {"type": "object", "properties": {"order_id": {"type": "string"}}}
}
],
"messages": [
{"role": "user", "content": "我的订单什么时候发货?"}
],
"thread": {"type": "create"}
}
第二轮,去掉 system 和 tools,只发新消息和上一轮的 id:
{
"model": "claude-opus-5-5",
"max_tokens": 1024,
"cache_control": {"type": "ephemeral"},
"messages": [
{"role": "user", "content": "标准快递一般几天?"}
],
"thread": {"type": "continue", "previous_message_id": "msg_01XyZ..."}
}
三个必须记住的约束:
- continue 请求里出现
system或tools,哪怕是null,都返回 400。 - thread 是 append-only 的,每个 response id 只能被 continue 一次。 想编辑或分支历史,只能新建 thread。
- 只发新消息。 API 不会拿你发的消息去和已存对话做比对,重复发送会让同一条消息在对话里出现两次,或者直接报错。
SDK 提供了 helper 把这些细节包住。client.beta.messages.thread(...) 会自动加 beta header、首轮 create、后续只发新消息、遇到 404 自动回放一次、refusal 之后自动新建 thread,并且在回放时自动去掉历史轮次上的 cache_control 断点。如果你不想自己管状态机,直接用 helper 是更省事的选择。
每轮该发什么:三类字段的分组规则
thread 最反直觉的地方是字段不是随便填的。官方把它们分成三组:
| 分组 | 字段 | 规则 |
|---|---|---|
| Create-only | system、tools |
只在 create 请求发,断点也放在这里。在 continue 里发,包括发 null,返回 400 |
| Fixed | model、tool_choice、thinking、mcp_servers(除每个 server 的授权 token)、context_management、output_config、container 里的 skills、顶层 cache_control、anthropic-beta 值集合 |
每个请求都要发,且值必须与 create 时一致。不一致返回 400 thread_fingerprint_mismatch,错误信息里会点名哪个字段不一样 |
| Free | max_tokens、stop_sequences、stream、metadata、service_tier 等 |
每轮可以改 |
需要特别注意的是 fixed 组里有几个看起来「应该能调」的字段。model 不能换,thinking 配置不能换,顶层 cache_control 也不能换。还有一个坑:anthropic-beta 的值集合也是指纹的一部分,你在某一轮多加一个无关的 beta header,就可能触发指纹不匹配。
mcp_servers 和 container 有个例外:API 不存 MCP server URL、授权 token 和 container id,所以这些每次请求都要带。授权 token 允许在轮次之间变化。
要改 create-only 或 fixed 字段,唯一办法是用新值重新 create 一个 thread,把完整对话再发一遍。
prompt cache 还在吗:在,而且和 thread 正交
这是本文标题里那个问题,值得单独说清楚。
prompt caching 是一个独立功能,有独立的文档、独立的计费、独立的断点规则。message threads 上线没有动它,官方也没有把它合并进 thread。两者作用在不同层面:
- thread 减少的是「每轮重复上传整段对话」这一层的工作量,直接收益是 TTFT 和请求体积。
- prompt caching 减少的是「重复前缀被重复计费」这一层的成本,直接收益是 token 单价。
thread 不会自动帮你缓存,因为缓存断点放在哪里本质上是一个业务决策:哪一段前缀稳定、哪一段每轮都变、要不要为静态 tools 开 1 小时 TTL。这些只有你知道,API 猜不出来。
还有个容易忽略的巧合:thread 在最后一次请求之后 5 分钟过期,prompt caching 的默认 TTL 也是 5 分钟,而且两者都是「被使用就续期」。所以它们的节奏天然对齐——如果你的 agent 循环间隔在 5 分钟以内,两个功能都活着;如果超过 5 分钟,thread 会先失效返回 404,缓存也可能已经凉了。这个约束把 message threads 的实际适用范围收得很窄:它服务的是紧凑的 agent 循环,不是那种用户隔几小时回来接着聊的客服会话。
怎么 enable:顶层字段和块级断点
prompt caching 有两种开启方式,任选其一,也可以组合。
方式一:顶层自动缓存。 在请求体顶层加一个 cache_control 字段:
{
"model": "claude-opus-5-5",
"cache_control": {"type": "ephemeral"},
"messages": [...]
}
系统自动把断点放在最后一个可缓存块上,并且随着对话增长自动前移。这是多轮对话最省心的写法,也正是 thread 文档里推荐的那种:在每个 thread 请求上都发同一个顶层 cache_control,这一轮从缓存读历史,同时把新的一轮写进缓存。
自动前移的实际效果是这样:第一轮写入的缓存覆盖到第一条回复为止,第二轮先命中这段前缀,再把断点推到第二条回复。
MERMAID_BLOCK_0
青色的块是已经进过缓存的前缀,黄色的块是这一轮新增的内容。只要青色的部分在下一轮一字不变,缓存就一直命中;改动其中任何一个块,哈希就变了,后面的缓存全部作废。这也是为什么带时间戳的 system 提示词会让缓存永远写、永远不命中。
方式二:显式块级断点。 把 cache_control 放在具体的内容块上,比如最后一个 tool 定义或者某条 system 消息:
{
"tools": [
{
"name": "get_order",
"description": "Look up an order by id",
"input_schema": {"type": "object"},
"cache_control": {"type": "ephemeral"}
}
],
"system": [
{"type": "text", "text": "...", "cache_control": {"type": "ephemeral"}}
]
}
缓存前缀的顺序是固定的:tools → system → messages,哈希逐层累计。也就是说,改任何一段在断点之前的内容,后面的缓存全部失效。
默认 TTL 是 5 分钟,命中即免费续期。需要更长的可以指定 1 小时:
{"cache_control": {"type": "ephemeral", "ttl": "1h"}}
价格倍数:5 分钟写入是基础输入价的 1.25 倍,1 小时写入是 2 倍,缓存读取是 0.1 倍(部分新模型更低)。所以缓存不是免费的,第一次写入比不缓存更贵,要至少命中一次才回本。静态的 tools 和长 system 通常第一次就回本,因为它们在每一轮都会被读到。
还有两个实操细节。断点处没命中时,系统会向后逐个块回看,最多检查 20 个位置,连续的 tool_use / tool_result 各算一个位置。另外,最小可缓存长度跟模型绑定:Claude Fable 5.1、Mythos 5.1、Opus 5.5、Opus 5、Sonnet 5.5 等是 512 token,Opus 4.8、Sonnet 5、Sonnet 4.6、Sonnet 4.5 是 1024 token,Opus 4.6 和 4.5 是 4096 token。短于这个长度,请求照常处理,但不会缓存,也不报错,你只能从 usage 里两个缓存字段都是 0 来判断。
thread 上的 4 个断点怎么算
这一节是 thread 和 prompt caching 交叉时唯一真正复杂的地方,官方规则有四条:
- 带
thread的请求最多 4 个cache_control断点,顶层那个算 1 个。 - create 请求里放在
system和tools上的断点,在每一次 continue 时都会重新计数。 - 历史消息上的断点不计数,因为它们不会在 continue 请求里被重新发送。
- 断点发出之后不能移动也不能删除,因为 continue 不能重发历史消息。
把这几条合起来看,一个很自然的配置会踩坑:你在 create 时给 3 个 tool 定义各放一个断点,再加上顶层 1 个,第一轮是 4 个刚好。到第二轮,那 3 个 tool 断点重新计数,加顶层还是 4 个,勉强活着。但只要再多一个 tool 断点,就会直接返回 400:
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "thread: a maximum of 4 blocks with cache_control may be provided when `thread` is set. Found 5.",
"details": {
"error_code": "thread_unsupported_request",
"unsupported": ["cache_control"]
}
}
}
所以实践上更稳的做法是:顶层 cache_control 一个,tools 或 system 上最多再放三个,把 4 个槽位用在真正稳定、真正长的前缀上。如果你要回放完整对话新建 thread,记得把历史消息上的断点去掉,只在最后一个块保留一个,否则回放请求自己就会超限。
还有一个 SDK 行为值得知道:helper 在回放或 fork 时会自动去掉历史轮次的断点,但你自己新加的消息、system、tools 和顶层 cache_control 会原样发出。也就是说,如果你手工往历史里塞过断点,换成 helper 也不一定干净,最好在设计阶段就控制住。
什么时候会失效
thread 的失败路径比正常路径更需要提前设计,因为它的错误大多是「下一次请求才知道」。
404 thread_not_found 表示 API 无法继续这个 thread,但不会告诉你原因。常见原因有六种:thread 过期;上一轮被服务但没被存储(比如响应以被 max_tokens 截断的 tool call 结束);上一轮由 fallback 模型写的;上一轮还在生成;请求来自不同的组织或 workspace;请求来自不同的用户或 service account。还有一个隐蔽的:新消息里加了已存对话无法接受的内容,比如一段本来没有图片的对话里第一次出现图片。
400 的三种:thread_fingerprint_mismatch 是固定字段变了;thread_already_continued 是同一个 response id 被 continue 了第二次;thread_continue_in_flight 是同一个 id 的上一个 continue 还在跑;thread_unsupported_request 是用了 thread 不支持的功能。
refusal 之后不要 continue。 API 可能已经把被拒绝的那一轮存下来了,而 continue 无法删除它。正确做法是新建 thread,把导致拒绝的那一轮删掉或改写。
fallback 模型写的轮次不会被存储。 你在 usage.iterations 里看到 fallback_message,就说明这一轮的 id 不能用来 continue,必须把完整对话(包括这一轮的回复)重新 create。流式响应里这个字段在最后一个 message_delta 事件里。
流式必须等 message_stop。 response id 在 message_start 就到了,但这时不能 continue。流中断后重试同一个 continue,可能收到 thread_already_continued 或 thread_continue_in_flight,这时回放完整对话是最安全的路径。
不支持的功能:Message Batches、Token counting、Compaction、Context editing(除了 clear_thinking_20251015 配 keep: "all")、HIPAA、Inference hooks。带 thread 的请求碰到这些,多数会返回 400,error.details.unsupported 会说明是哪一项。
一次 continue 在服务端发生了什么
把 normal path 和 replay path 画在一起,结构就清楚了:
MERMAID_BLOCK_1
关键点是 rebuild full prompt 这一步。thread 并不是把 prompt 缓存在服务端直接复用,而是把历史消息重新拼成完整 prompt 再调用模型。所以模型看到的输入和你不带 thread 时完全一样,token 也照常计费。thread 省掉的是「你上传 → API 校验 → 分词」这一段,而不是「模型读」这一段。
拼接完成之后才会做前缀哈希查询缓存,所以 prompt caching 在这个位置上是叠加生效的:thread 负责少传,cache 负责少算钱。
横向比较:thread、缓存和客户端裁剪
| 方案 | 机制 | 降低 TTFT | 降低 token 成本 | 主要失效条件 |
|---|---|---|---|---|
| message threads | 服务端短期保存对话,后续只传增量 | 是,长请求中位约 35% | 否,计费不变 | 空闲 5 分钟、跨组织/账号、不支持的功能 |
| prompt caching | 缓存稳定前缀,命中按 0.1x 计费 | 部分,主要省前缀预处理 | 是 | 前缀变动、未达最小长度、断点放错位置 |
| 客户端裁剪/摘要 | 自己删旧消息、压缩工具结果 | 是 | 是 | 信息损失,摘要质量不可控 |
| 什么都不做 | 每轮上传完整对话 | 否 | 否 | 上下文窗口上限 |
三者不互斥。客户端裁剪决定「上下文里留什么」,thread 决定「这一轮传多少」,prompt caching 决定「重复前缀怎么计价」。一个成熟的 agent 通常三层都用:裁剪控制窗口,thread 降低传输,cache 降低成本。
成本与收益:TTFT 降 35%,账单不变
官方给的性能数字是:500k 到 1M token 的请求,首 token 延迟中位数下降约 35%。这个区间的选择很说明问题——只有在超长上下文里,重复上传和预处理的耗时占比才足够大,35% 才有意义。短对话里你看不出差别,甚至可能因为多一次服务端查找而略慢。
计费方面文档写得很清楚:threads 不改变定价,也不改变速率限制。输入 token 该多少还是多少,因为模型仍然读完整对话。
所以真实的收益组合是这样的:
- 只开 thread:首字更快,请求体更小,账单一分不少。
- 只开缓存:账单下降,但每轮仍要上传完整对话,长上下文下上传耗时仍在。
- 两个都开:首字更快,账单也下降。
缓存这边的账要算清楚。5 分钟 TTL 的写入是 1.25 倍基础价,读取是 0.1 倍。假设一段 20k token 的稳定前缀被命中 3 次,不缓存的总成本是 4 次 × 1.0;缓存之后是 1 次 × 1.25 + 3 次 × 0.1 = 1.55,大约省 61%。如果只命中 1 次,写入加读取是 1.35,反而比不缓存更贵。所以缓存的收益完全取决于「同一段前缀被复用的次数」,而 thread 恰好保证了后续轮次的前缀是稳定的——这两个功能在这一点上是互相成全的。
失效边界:什么场景不该用
间隔超过 5 分钟的会话。 thread 空闲 5 分钟就过期,用户隔一会儿回来接着聊的客服场景几乎每轮都要 replay,收益被回放成本吃掉。这类场景应该用缓存,而不是 thread。
需要编辑或分支历史。 thread 是 append-only,每个 id 只能 continue 一次。想做「回到第 3 轮改一句话重来」,只能新建 thread 并重发完整对话。
需要压缩上下文。 thread 不支持 compaction,也不能替换历史轮次。上下文快满时,你得自己写摘要,然后新建 thread。
合规场景。 thread 不属于 ZDR,也不适用于 HIPAA readiness 组织。对话内容会在服务端内存里存活最多 1 小时,这对某些行业是不可接受的。
多实例、多密钥。 thread 跟创建它的组织、workspace、用户或 service account 绑定。如果你的 agent 是多副本部署、请求会打到不同实例,或者轮换 API key,continue 很容易变成 404。用 workspace key(legacy)创建的 thread 会一直留在那个 key 上。
批处理。 Message Batches 不支持 thread,带 thread 的 batch item 会直接以 invalid_input_error 结束。
还有一个不是 bug 但容易误判的行为:thread 是 best-effort 的,create 和 continue 的响应都不会告诉你这一轮有没有被存下来。所以「偶发 404」不是异常,而是设计的一部分。生产环境必须假设任何一次 continue 都可能失败,并且随时能回放完整对话。
参考文档
- Message threads 文档 — create / continue / replay 语义、字段分组、错误码、prompt caching on a thread
- Message threads 公告 — 发布背景、TTFT 数据、Claude Code 默认开启
- Prompt caching 文档 — 自动缓存、显式断点、TTL、价格、最小可缓存长度
- Prompt caching 定价 — 5 分钟 / 1 小时写入与读取倍数
- Messages API 参考 — 请求与响应字段
- Refusals and fallback — fallback 与 thread 的交互、sticky routing
- Context editing — thread 支持与不支持的上下文管理能力
- API and data retention — ZDR 与 HIPAA 覆盖范围
- Claude Managed Agents session threads — 同名但不同的概念
- Streaming —
message_start与message_stop的时序要求
你的 agent 循环间隔大概多久?如果超过 5 分钟,thread 基本用不上,评论区聊聊你怎么处理。
作者: itech001
来源: 公众号:AI人工智能时代(the-ai-era)
网站: https://www.theaiera.top/
关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top
本文首发于 AI 人工智能时代,转载请注明出处。

浙公网安备 33010602011771号