会话留在服务端:Anthropic Message Threads 拿走了什么,又把什么推回给你
会话留在服务端:Anthropic Message Threads 拿走了什么,又把什么推回给你
【摘要】11 月 2 日,Anthropic 给 Messages API 开了一个叫 Message Threads 的公测。你第一次发完整对话,服务端把它存下来;之后每轮只发新增的那几条。Anthropic 说 50 万到 100 万 token 的请求,首字延迟中位数降了约 35%。听起来像白捡的性能,但读完官方文档我发现三件事:token 用量一点没少;客户端必须自己留一份完整副本;而 5 分钟的存活期,跟 agent 干活的节奏正面冲突。这篇讲清楚它到底省了什么、没省什么,以及什么样的 agent 用得上、什么样的用不上。
目录
一、一个把"每轮重发"变成"只发增量"的 API
二、它省的是上传,不是阅读
三、35% 这个数字是怎么来的,以及它对谁成立
四、最容易被忽略的那条:客户端才是真相之源
五、5 分钟 TTL:一个 agent 循环的死穴
六、fingerprint 一致性,和"按任务调档"的冲突
七、还有一堆不能用的地方
八、怎么判断你的 agent 该不该上
一、一个把"每轮重发"变成"只发增量"的 API
先说清楚 Messages API 原来的样子:它是无状态的。
不管你和模型聊了 50 轮还是 500 轮,每一次调用,你都要把从第一句 user 消息开始的全部历史重新发一遍——包括模型自己说过的每一句、每一个 tool_use、每一个 tool_result。服务端收到这一大坨,重新校验、重新 tokenize,然后才轮到模型开始思考。
Anthropic 这次的改法很简单:加一个 thread 参数。
POST /v1/messages
anthropic-beta: message-threads-2026-08-12
{
"model": "claude-opus-5-5",
"system": "...", // 只在第一次发
"tools": [...], // 只在第一次发
"messages": [...], // 第一次:完整对话;之后:只发新增
"thread": {"type": "create"} // 第一次
}
第一次请求带 {"type": "create"},服务端把整个对话(含 Claude 自己的回复)存在响应 id 下。之后每次只发新增消息,带上 {"type": "continue", "previous_message_id": "<上一次响应的 id>"},system 和 tools 要省略——服务端留着。
就这么点事。但它落在一个很具体的痛点上:一个 coding agent 跑一次会话可能有几百次请求,每次都把前面所有 tool_result 重新传一遍。累积到几十万 token 时,光是"传上去 + 重新 tokenize"这段就成了首字延迟的主要来源。
Anthropic 给的数字是:50 万到 100 万 token 的请求,首字延迟中位数下降约 35%。
二、它省的是上传,不是阅读
这是整件事里最容易被误读的一点,官方文档写得很直白:
While each request is smaller with message threads, Claude still reads the full conversation on every turn, so token usage stays the same.
请求变小了,但模型读的还是全部。
也就是说,省掉的是:网络传输时间、服务端重复的 tokenize 开销、前缀重复解析的开销。没省掉的是:模型侧的推理成本、上下文占用的显存、以及按 token 计的费用。
这跟 prompt cache 是两个正交的层。Prompt cache 省的是"前缀不用重算";threads 省的是"前缀不用重传"。你可以两个一起用——文档里顶层 cache_control 允许每轮带,而且要求每轮值一致。
但这里有个必须说清楚的后果:如果你原来靠"每轮重发"来自动续上 prompt cache,上了 threads 之后这个隐式行为没了。 你得显式管理 cache_control 的位置和生命周期。少一个东西,通常意味着多一个你要自己管的东西。
三、35% 这个数字是怎么来的,以及它对谁成立
Anthropic 的原话有一个限定条件:
with message threads on by default for most Claude Code users, requests with 500k to 1M tokens see median TTFT drop by about 35%
三个限定词:默认开启(不是所有人)、50 万到 100 万 token(不是所有规模)、中位数(不是平均数也不是最坏情况)。
几个推论:
- 上下文小的 agent 基本感觉不到。 几千 token 的对话,重传的量本来就小,节省自然也小。文档自己说"the more context an agent carries, the more message threads can reduce time to first token"。
- 上下文大的 agent 收益大,但那些 agent 恰恰是最容易撞上 TTL 的(后面第五节会说)。
- 中位数下降 35% 意味着长尾没改善。 如果你的场景是偶发一个超长请求,前几次延迟可能反而更难看(要建 thread)。
另外有一条被很多人忽略的官方提示:Anthropic 没有给出 threads 单独的 TTFT 数字,"35%“是"默认开启前后"的对比,不是"threads 开 vs 关”。这个区别在你自己做 A/B 的时候要注意。
四、最容易被忽略的那条:客户端才是真相之源
官方文档里有一句话,位置很靠后,但它是整个设计的地基:
Your application stays the source of truth for the conversation. Keep your own copy of every message, and build the replay path from the start.
翻译过来:服务端那个 thread 是缓存,不是存储。你必须自己在本地留一份完整对话。
原因很实在——线程是 best-effort 的。它会过期,会被删,可能在服务端某个时刻消失。而文档明确说了:
No response, to a create or a continue request, says whether the API stored that turn. You find out on the next continue request.
你无法知道服务端到底存没存。 只有下一次 continue 请求的时候才会通过 404 告诉你。
这带来两个具体的坑:
第一,SDK 的 helper 只在内存里存。 文档说得很清楚:SDK thread helper “keeps this copy in memory while the helper object exists, and saves it nowhere else”。进程一挂,你的"真相之源"就没了。 如果对话要跨进程、跨机器、断点续跑,你得自己实现持久化。
第二,replay 路径要一开始就搭好。 不是出错了再补——是第一次写代码时就把"完整重发"这条路径做出来。我见过太多"先跑通再补容错"的代码,最后补不上去,因为原始对话根本没存。
五、5 分钟 TTL:一个 agent 循环的死穴
线程的寿命规则:
- 最后一次请求后 5 分钟过期
- 最后一次使用后不超过 1 小时删除
- 每存一个 response 就重置那 5 分钟
对于一个交互式对话,这够用。你说一句,它答一句,间隔不会超过 5 分钟。
但 agent 不是这样工作的。
一个 coding agent 的典型循环是:读文件 → 改代码 → 跑测试 → 测试跑了 8 分钟 → 看到失败 → 改代码 → 再跑测试。
那 8 分钟里,它一次请求都没发。线程在第 5 分钟过期。等它跑完测试回来要发下一个请求时,拿到的是 404。
404 之后怎么办?重发完整对话。
所以你会看到一个很荒谬的场景:agent 干了 20 分钟活,每次超过 5 分钟的工具调用之后都要把整个上下文重新传一遍。 而这恰恰是 threads 最应该帮上忙的场景——恰恰是那种"上下文大、工具调用多"的长任务。
这不是 bug,这是 TTL 和 agent 工作节奏之间的结构性冲突。文档也承认了这点,说线程"best for active sessions where latency matters"。
所以我判断 threads 的真实适用面比公告暗示的窄:它最适合"高频短间隔"的 agent 循环(比如每轮就是一个 tool call,间隔几秒),不适合"跑长任务、长时间不通信"的 agent。
绕过的办法有两个:把长任务拆碎,每 5 分钟内插一次请求;或者自己实现服务端缓存。
六、fingerprint 一致性,和"按任务调档"的冲突
这是文档里最容易被忽略、但对实际开发最要命的一条规则。
thread 创建时,有一批字段被锁定了:
| 分类 | 字段 | 规则 |
|---|---|---|
| 只在 create 发 | system, tools |
continue 上发送(哪怕是 null)→ 400 |
| 每轮必须一致 | model, tool_choice, thinking, mcp_servers(除授权 token), context_management, output_config, container 的 skills, 顶层 cache_control, anthropic-beta 值 |
值不同 → 400 thread_fingerprint_mismatch,错误消息会指出哪个字段不同 |
| 自由可变 | max_tokens, stop_sequences, stream, metadata, service_tier 等 |
每轮可改 |
先看"只在 create 发"这条。直觉上"每轮都发一遍 system 和 tools 最保险",但发了就 400——哪怕发的是 null。这跟大多数 API 的直觉相反。
再看第二条,这条是真冲突。
thinking 在锁定列表里。model 也在。
而现在的 agent harness,一个非常常见的做法是根据任务难度调整推理档位——简单任务用低档省成本,复杂任务用高档保质量。或者根据错误类型换模型。
这两个需求跟 threads 直接冲突。 你想在第二轮把 thinking 从 low 调到 high,拿到的是 400 thread_fingerprint_mismatch。
这个错误设计得还算友好——它告诉你哪个字段不一样。但它把一个决策提前了:如果你要按任务调档位,你得在建 thread 之前就知道档位,而且不能中途改。
(顺便说,max_tokens 是自由的,所以"输出多长"可以调,"想多久"不能调。这两个在实践中经常被混为一谈。)
七、还有一堆不能用的地方
文档专门列了一节"不可用",信息量不小:
和 compaction 互斥。 按需压缩对话的 compaction 参数不能用,compact-2026-09-04 beta 头也不能用。也就是说,长对话的主动压缩这条路暂时被堵死了。而 threads 的目标场景恰恰是长对话——上下文压缩和 thread 是两种不同的长对话解法,现在你只能选后者。
token counting 不支持。 你没法问"这个 thread 现在多少 token 了",只能自己数完整对话。
Message Batches 不支持。 批处理里带 thread 的项会报 invalid_request_error。
context editing 大部分不支持(只留 clear_thinking_20251015 配 keep: "all")。
HIPAA readiness 的组织用不了。开了 Inference hooks 的组织也用不了。
没有管理 API。 不能列出、读取或删除一个 thread。这意味着你没法审计服务端到底存了你什么,也没法主动清理。
还有两个行为细节容易踩:
- thread 是 append-only 的,每个 response id 只能被续一次,而且必须从最新的 id 续。想编辑早期消息或从中间分支,只能开新 thread。
- 重复发消息会重复出现。API 不检查你发的消息是否和已有的重复。文档给的例子是"以 previous response id 开头的 continue 请求会报错"——但报错的是这个特例,不是通例。客户端必须自己保证增量正确。
八、怎么判断你的 agent 该不该上
三个问题,对应三个门槛:
第一,你的 agent 会不会有超过 5 分钟不通信的空窗?
会——比如跑长测试、跑构建、等外部 API——那 threads 的价值会被 TTL 吃掉大半。先量化一下你的 agent 一次会话里的最长空窗有多长。
第二,你的上下文有多大?
几千 token 的,收益接近零。上 threads 之前先看一眼你的典型请求有多大。文档给的收益区间是 50 万到 100 万 token,那已经是非常长的 agent 会话了。如果你还没到这个量级,先把上下文管理做好,比优化传输划算得多。
第三,你的 agent 会不会中途换模型或调推理档位?
会——那 fingerprint 规则会直接卡住你。要么放弃 threads,要么把决策提前。
如果三个都是"不会",那 threads 对你几乎是纯收益:少传很多数据,首字延迟降一截,token 账单一分不变。
最后提醒一句实现顺序:先做 replay 路径,再开 threads。 官方文档把这条写在"从第一天就搭好"的位置上,我认为是整篇文档最实用的一句。它不是可选项——它是 threads 能安全使用的前提。
一句话:Message threads 把"每轮重发"这个动作从客户端挪到了服务端,省掉了上传和 tokenize,但没省 token、没省模型的阅读、也没省你维护状态的义务。它是一次干净的架构改良,不是性能魔法。
留言区:你在用 threads 吗?你的 agent 一次会话最长有多久不通信?如果踩过 thread_fingerprint_mismatch,说说当时想调的是什么。
- 点赞
- 收藏
- 关注作者
评论(0)