Claude Message Threads 经过网关会断在哪
最后核实:2026-11-03。对照 Anthropic 的 API release notes 和 Message threads 文档。功能还在 beta,字段和行为可能会改,动手前请以官方文档为准。
直接回答
2026-11-02,Anthropic 在 Messages API 公测 Message Threads(beta 头 message-threads-2026-08-12):第一轮发完整对话,之后每轮只发新消息加上一轮回复的 id,由服务端拼回整段对话。它对直连很友好,但对网关和代理有几个硬约束:同一个 thread 必须始终落在同一个 Key / workspace 上,否则返回 404;请求体里的 thread 字段必须原样转发,丢了不会报错,只会让模型在没有前文的情况下回答;model、thinking、anthropic-beta 等“固定字段”在整条 thread 里不能变。下面逐条拆开,并给出网关侧和客户端侧的修法。
30 秒看懂 Message Threads
| 步骤 | 你发什么 | 要点 |
|---|---|---|
| 创建 | 完整对话 + system + tools,加 "thread": {"type": "create"} | 服务端把对话连同这轮回复存在回复的 id 下 |
| 续接 | 只发上一轮之后的新消息,加 "thread": {"type": "continue", "previous_message_id": "<上一轮 id>"} | 不能再带 system / tools(带了哪怕是 null 也报 400) |
| 重放 | 续接返回 404 thread_not_found 时,用自己保存的完整对话重新发一个创建请求 | thread 是“尽力而为”:最后一次请求 5 分钟后过期,最迟 1 小时删除 |
官方的定位是让长对话和智能体循环请求更小、首字更快:不用每轮都重新上传、校验和分词整段对话。有三点容易误解:
- 对话的“真相”仍在你这边。 文档要求客户端保留每一条消息的副本,并且从一开始就写好重放路径。任何响应都不会告诉你这一轮有没有被存下来,要等下一次续接才知道。
- thread 不等于缓存。 文档原话的意思是:thread 不会替你缓存提示,缓存断点仍由你决定(见下文“thread 和提示缓存”)。
- 计费规则文档里没有单独写。 本文不对“用 thread 是否省钱”下结论,以响应里的
usage为准。
另外,Anthropic 开发者账号 @ClaudeDevs 在 X 上称 Claude Code 已开始使用 threads,并给出了长上下文请求首字时间明显下降的数字。截至 11-03,官方文档里没有对应说明,这一条请按单一来源看待。可以确定的是:文档写明 SDK 的 beta tool runner 默认就会使用 thread(可在创建时关闭)。也就是说,你没主动开启 thread,请求里也可能已经带着它了。
字段分三类
这张表是排查网关问题的核心,整理自官方文档的 “What to send on each request”:
| 类别 | 字段 | 规则 |
|---|---|---|
| 仅创建时发送 | system、tools | 只在创建请求里发;续接请求里出现即 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 等 | 每轮可以不同 |
网关和代理最容易踩的 7 个坑
直连 Anthropic 的用户只要用官方 SDK 的 thread helper,基本不用操心。问题集中在“客户端 → 网关 → 上游”这条链路上。网关为了负载均衡、容灾和计费所做的那些常规动作,恰好和 thread 的约束冲突。
| # | 网关的常规行为 | 对 thread 的影响 | 客户端看到的现象 |
|---|---|---|---|
| 1 | 在多个上游 Key / workspace 之间负载均衡 | 续接请求落到另一个 Key 上,文档列明“请求来自不同的 organization、workspace、用户或服务账号”会 404 | 频繁 404 thread_not_found;SDK 自动重放后能用,但每轮都在全量重发,等于没开 thread |
| 2 | 按固定结构体解析再序列化请求体 | 结构体里没有 thread 字段,就会被静默丢弃 | 见下文“最危险的一种” |
| 3 | 改写或追加 anthropic-beta,或做模型映射 / 自动降级 | beta 取值集合和 model 都是固定字段,前后两轮不一致就会冲突 | 400 thread_fingerprint_mismatch |
| 4 | 上游出错时换渠道 / 换 Key 重试 | 每个回复 id 只能续接一次;第一次请求可能其实已经在上游跑完或还在跑 | 400 thread_already_continued 或 thread_continue_in_flight;换了 Key 还会叠加第 1 条 |
| 5 | 把上游错误统一改写成自己的格式 | error.details.error_code 丢失,SDK 判断不出是 thread_not_found,无法自动重放。注意“模型不存在”也返回 404,只看状态码会误判 | 404 直接抛给业务代码,或被当成“模型不存在” |
| 6 | 只提供 OpenAI 兼容端点,内部再转成 Anthropic 格式 | Chat Completions 里没有能表达 thread 的字段 | 无法使用;只有 Anthropic 原生 /v1/messages 才能用 thread |
| 7 | 上游没回用量时按请求体本地估算计费 | 续接请求只有新消息,体积可能只是原来的零头,本地估算会严重偏低 | 账单和实际用量对不上;网关应以上游返回的 usage 为准 |
最危险的一种:thread 字段被静默丢掉
前 6 种坑大多会报错,第 2 种却往往不报错。
续接请求的设计就是“只发新消息、不带 system / tools”。如果网关把请求体反序列化成一个预先定义好的结构体,结构体里又没有 thread,这个字段会在转发前被丢掉。上游看到的就是一个普通请求,内容只有最后一两条消息:
- 新消息以
tool_result开头(智能体循环里最常见):前面没有对应的tool_use,上游返回 400,至少能发现。 - 新消息是一条普通
user消息:上游照常回答,只是没有系统提示、没有工具、也没有之前的任何上下文。回答看起来“有点怪”,但没有任何报错。
这不是假设。以开源网关 new-api 为例,v1.0.0-rc.46 的源码里:anthropic-beta 请求头会被合并后原样转发,但 ClaudeRequest 结构体里没有 thread 字段。没开启请求体透传的渠道,thread 就会在这一步被丢掉。其他按结构体实现 Anthropic 接口的代理也值得自查一遍。
客户端怎么自检: 续接回合如果明显“忘了前文”,或者 usage 里的输入规模(input_tokens + cache_read_input_tokens + cache_creation_input_tokens)只和你这一轮发的几条新消息差不多大,基本可以判断 thread 没有生效、上游只看到了片段。文档没有逐项说明 thread 下 usage 的口径,这条判断属于经验推断,用来报警足够,不要拿来对账。
网关侧怎么改
如果你维护的是网关或代理,最小改动清单如下:
thread原样透传。 结构体加上这个字段,或者对/v1/messages走请求体透传。上游如果不支持 thread,宁可返回 400 并带上thread_unsupported_request这类明确的错误码,也不要静默丢掉。这样官方 SDK 设了unsupported_request_behavior: continue_unthreaded时就能自动退回不带 thread 的全量请求。- 按
previous_message_id粘滞。 创建请求成功后,记录“回复id→ 上游渠道和 Key”的映射;续接请求按previous_message_id查表,路由回同一个 Key。映射的 TTL 取 1 小时就够,对应 thread 的最长保留时间。每一轮的新回复id都要写入映射,因为下一轮会从它续接。 - 续接请求不跨 Key 重试。 原 Key 失败时,把错误原样返回给客户端,让客户端用完整对话重新创建。不要自作主张换一个 Key 重发续接请求,那只会把错误变成 404 或
thread_already_continued。 - 错误原样返回。 保留上游的 HTTP 状态码、
error.type和error.details(尤其是error_code和unsupported)。SDK 的自动重放完全依赖这些字段。 - 固定字段别按请求变。 不要按请求动态追加 beta 值;模型映射在整条 thread 内保持一致;带
thread的请求不要做跨模型降级。 - 计费以上游
usage为准。 对带thread的请求关掉本地估算,或者在上游没有返回用量时明确记为异常。
# 创建:完整对话 + system + tools
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: message-threads-2026-08-12" \
-H "content-type: application/json" \
-d '{"model":"claude-opus-5-5","max_tokens":1024,
"cache_control":{"type":"ephemeral"},
"system":"You are a code reviewer.",
"messages":[{"role":"user","content":"Review this diff: ..."}],
"thread":{"type":"create"}}'
# 续接:只发新消息;不带 system / tools;固定字段(model、顶层 cache_control、beta 头)保持一致
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: message-threads-2026-08-12" \
-H "content-type: application/json" \
-d '{"model":"claude-opus-5-5","max_tokens":1024,
"cache_control":{"type":"ephemeral"},
"messages":[{"role":"user","content":"Now check the error handling."}],
"thread":{"type":"continue","previous_message_id":"'"$MESSAGE_ID"'"}}'经过网关时,把 https://api.anthropic.com 换成网关的地址,请求本身保持不变。网关要做的是让第二个请求落到和第一个请求同一个 Key 上。
客户端侧怎么防
- 优先用官方 SDK 的 thread helper。 它会自动加 beta 头,续接时自动省掉
system/tools,遇到thread_not_found、thread_already_continued、thread_continue_in_flight时用完整对话重放一次,并在降级模型回答或被拒答之后自动开新 thread。 - 把
unsupported_request_behavior设为continue_unthreaded。 网关或组织设置不支持 thread 时,自动退回普通请求,而不是每轮都重放。 - 自己保留完整对话。 helper 只把对话保存在内存里,进程退出就没了。需要跨进程的会话请自己落盘。
- 流式响应等到
message_stop再续接。id在message_start里就能拿到,但提前续接可能得到 404。流中途断开时,先用同一个续接请求重发一次;如果返回thread_already_continued或thread_continue_in_flight,再走全量重放。 pause_turn用空消息续接。 回复以stop_reason: "pause_turn"结束时,用"messages": []续接;不要在暂停期间插入新的 user 消息。
thread 和提示缓存
很多人看到“请求只发新消息”,就以为不用再管缓存了。文档写得很明确:thread 不替你缓存。
- 最简单的做法是每个请求都带同样的顶层
cache_control。API 会把断点打在本轮最后一个可缓存的块上,于是每轮都从缓存读取前文,并把新一轮写进缓存。顶层cache_control本身也是固定字段。 - 一个带
thread的请求最多 4 个缓存断点,顶层cache_control算 1 个;创建请求里system/tools上的断点,在每个续接请求里都要再算一次。超出返回 400thread_unsupported_request。 - 断点发出去以后就不能移动或删除,因为续接请求无法重发之前的消息。重放时要先去掉旧消息上的断点,只在最后一块保留一个。
缓存的经济账没有变:Anthropic 的缓存写入是输入价的 1.25 倍(5 分钟 TTL)或 2 倍(1 小时 TTL),读取是 0.1 倍。断点放错位置、命中率又低,费用反而比不缓存更高。原理和算账方法见提示缓存详解。对网关来说还有一层:缓存也是按上游账号维度存的,第 2 条的 Key 粘滞同样决定缓存命中率,一次做好,两个问题一起解决。
不能用 thread 的场景
按官方文档(2026-11-03):
- Message Batches:带
thread的条目会在结果里以invalid_request_error结束。 - Token 计数:带
thread返回 400,需要用完整对话(含system/tools)计数。 - Compaction:按需压缩不能和 thread 一起用;要压缩,就自己写摘要,用摘要开一个新 thread。
- Context editing:只支持
clear_thinking_20251015配keep: "all"。 - 组织设置:开启了 HIPAA readiness 或 Inference hooks 的组织不能用;功能本身不符合 ZDR。
- 平台:支持 Claude API、Claude Platform on AWS、Google Cloud(Anthropic 托管)和 Microsoft Foundry;Amazon Bedrock 不在列表里。
- 没有管理接口:不能列出、读取或删除 thread。
来源
- Anthropic API release notes(2026-11-02 Message threads 条目):https://platform.claude.com/docs/en/release-notes/api
- Message threads 文档:https://platform.claude.com/docs/en/build-with-claude/message-threads
- Prompt caching 文档:https://platform.claude.com/docs/en/build-with-claude/prompt-caching
- new-api 源码(v1.0.0-rc.46 的
ClaudeRequest结构体和 Claude 渠道适配器):https://github.com/QuantumNous/new-api
“Claude Code 已接入 thread”和具体的首字时间提升数字只出自官方 X 账号的帖子,文档中尚无对应说明,本文不把它当作定论。
常见问题
Claude Message Threads 是什么?
Anthropic 于 2026-11-02 在 Messages API 公测的功能,beta 头为 message-threads-2026-08-12。第一轮带完整对话并加 thread: {type: "create"},之后每轮只发新消息并加 thread: {type: "continue", previous_message_id},由服务端拼回整段对话。thread 在最后一次请求 5 分钟后过期。
为什么经过网关调用时,续接请求会返回 404 thread_not_found?
文档列出的原因包括:thread 已过期、上一轮没有被存储、上一轮由降级模型回答、上一轮还在生成,以及请求来自不同的 organization、workspace、用户或服务账号。网关在多个上游 Key 之间轮换,就会命中最后一条。修法是按 previous_message_id 把续接请求粘到同一个 Key 上。
用了 thread 还需要提示缓存吗?
需要。thread 不替你缓存;每个请求带同样的顶层 cache_control 是最简单的做法。一个带 thread 的请求最多 4 个断点。
网关把 thread 字段丢掉会发生什么?
上游收到的是一段没有前文、没有系统提示和工具的普通请求。以 tool_result 开头时会报 400;以 user 消息开头时会照常回答,但已经丢了上下文,而且不会报任何错误。
哪些情况不能用 Message Threads?
Message Batches、Token 计数、按需压缩、大部分 context editing;开启了 HIPAA readiness 或 Inference hooks 的组织;需要 ZDR 的场景;以及 Amazon Bedrock。
