当前位置:首页 > 编程开发 > 正文

Anthropic message threads:20 轮少传 96%

约 5051 字 · 预计阅读 13 分钟
一句话总结:Anthropic 在 Messages API 里开放了 message threads 测试,做法是把会话状态留在服务端,客户端首个请求带完整对话,之后每次只发新增消息加一个响应 ID。我按每轮 500 token 模拟了 20 轮,累计传输从 99,000 降到 4,000,降幅 96%。官方给的数据是 50 万到 100 万 token 的请求首 token 延迟降约 35%。但 token 账单不会减少,省的是传输和预填充。

写长跑型 Agent 的人大概都踩过这个坑:任务跑了三十轮,每轮都要把前面三十轮的对话原封不动再发一遍。

模型那边看到的是一份完整的对话,但网络这边传的是一吨重复数据。第 30 轮的时候,请求体里 99% 的内容服务器早就见过。

Anthropic 上周放出来的 message threads 就是冲着这件事。

一、message threads 解决的是什么:重复传输

先说清现状。

现在调大模型 API,是无状态的。每次请求都要把整段对话塞进 messages 数组发过去,服务器处理完就忘。第 n 轮的请求体里,前 n-1 轮的内容一个字不少。

这带来两个成本:

网络传输。 一段 20 轮的对话,上下文长度 9,700 token,第 20 轮就要发 9,700 token 出去。前面 19 轮的 9,500 token 全是重复的。

预填充。 服务器收到之后要把这些 token 重新编码一遍,才能开始生成。这一段耗时就叫 TTFT(首 token 时间)。

Agent 场景把这个问题放大了。一个跑 50 步的任务,每步都带上历史,总传输量是串行累加的。脚本模式那篇算过类似的事:换一种调用方式,20 步的 token 消耗能差 27 倍。

二、threads 的做法:会话只发一次

机制不复杂。

第一个请求:携带完整会话内容,同时开启一个 thread。服务器把这次会话的状态保存到内存里,返回一个响应 ID。

后续请求:只发新增的那条消息,加上之前的响应 ID。服务器凭 ID 找到对应状态,接着往下算。

状态的生命周期:保存在内存中,加密,不落盘,末次请求之后 5 分钟过期。

Anthropic message threads:20 轮少传 96%

图 1:同一个 20 轮对话,旧方式每轮重发全部历史,threads 只发新增那一条

官方说这能力对多数 Claude Code 用户已经默认开启。受影响最大的场景是长跑编码 Agent、每一步都带截图的 computer use、以及文档和图片密集的工作流——这三类的共同点是上下文涨得特别快。

三、我按 20 轮算了一遍会话传输量

光看机制没感觉,我写了个脚本把两种方式的传输量算出来。

口径:每轮用户发 200 token,模型回 300 token,跑 20 轮。

USER_TURN, BOT_TURN, ROUNDS = 200, 300, 20

def old_way():
    history = send = prefill = 0
    for _ in range(ROUNDS):
        req = history + USER_TURN
        send += req
        prefill += req          # 全量重新编码
        history += USER_TURN + BOT_TURN
    return send, prefill

def threads_way():
    history = send = prefill = 0
    for i in range(ROUNDS):
        if i == 0:
            req = history + USER_TURN
            send += req
            prefill += req      # 首轮仍要全量
        else:
            send += USER_TURN   # 只发新增
            prefill += USER_TURN  # 服务端复用已编码状态
        history += USER_TURN + BOT_TURN
    return send, prefill

跑出来的结果:

指标              旧方式      threads      降幅
累计传输          99,000        4,000     96.0%
累计预填充         99,000        4,000     96.0%
第 20 轮传输        9,700          200     97.9%

第 20 轮那一行最能说明问题:旧方式要发 9,700 token,threads 只发 200。差了 48 倍。

这个数字是我按固定轮长模拟出来的,真实对话的轮长不均,比例会有出入,但量级不会变。

四、省的不是 token,是预填充时间

这里有个特别容易搞错的地方,我得说清楚。

账单不会变。

模型在生成第 20 轮回复时,仍然要"看到"前 19 轮的全部内容——上下文窗口没变,计费的输入 token 数也没变。threads 改变的是这些内容怎么到位,不是到位多少。

这就是为什么官方强调的是 TTFT(首 token 延迟)降 35%,不是成本降 35%。真正决定成本的是上下文长度,27B 那篇算过的内存账讲的是同一件事:上下文越长,贵的那部分越往后压。

35% 这个数字对应的上下文区间是 50 万到 100 万 token。上下文越长,重复预填充的浪费越大,这个降幅越明显。短对话(几千 token)几乎感觉不到差别。

所以:

  • 省的是等待时间
  • 省的是带宽
  • 不省 token,不省钱

如果你的场景是"每次只问一句、几十 token",threads 对你没用。

五、threads 接口大概长什么样

官方还没放完整文档细节,我按公开描述把调用形状写出来。这段是示意,不是可直接跑的 SDK 代码。

import requests

class ThreadSession:
    def __init__(self, api_key, base):
        self.h = {"x-api-key": api_key, "content-type": "application/json"}
        self.base = base
        self.thread_id = None      # 首轮之后才有
        self.last_response = None

    def send(self, user_text):
        if self.thread_id is None:
            # 首轮:带完整会话,开启 thread
            body = {
                "messages": [{"role": "user", "content": user_text}],
                "thread": {"enabled": True},
            }
        else:
            # 后续:只发新增消息 + 上一轮响应 ID
            body = {
                "messages": [{"role": "user", "content": user_text}],
                "thread": {
                    "id": self.thread_id,
                    "after": self.last_response,
                },
            }
        r = requests.post(f"{self.base}/messages", json=body, headers=self.h, timeout=120)
        r.raise_for_status()
        data = r.json()
        self.thread_id = data.get("thread_id", self.thread_id)
        self.last_response = data["id"]
        return data["content"]

关键在于两件事:首轮要显式开启 thread;后续每次都要把上一轮的响应 ID 带回。

少了 after 那个字段,服务器找不到状态,就只能当新会话处理——这时候你既付了 threads 的麻烦,又没拿到省下的传输。

六、四个坑

按踩中概率排。

1. 5 分钟过期。 状态保存在内存里,末次请求后 5 分钟释放。Agent 里如果有"思考很久再发下一条"的环节,很容易过期。过期之后会退化成重新建立会话,你多付一次全量。给长间隔的步骤设个心跳,或者干脆别用 threads。

2. 状态在内存里,不落盘。 这意味着服务端一重启或做负载均衡迁移,会话就没了。生产环境必须处理"ID 失效"这个分支,不能假设 ID 一直有效。

3. 不能并发。 同一个 thread 是两个请求之间的状态,如果同时发两条消息进去,谁先谁后不定。多 Agent 并行时必须每个 Agent 一条独立 thread。

4. token 账单不变。 这条不算坑,但最容易误判。看到传输量降 96% 就以为成本降了 96%,预算会算错。

七、边界

官方公布的是 TTFT 中位数降约 35%(50 万到 100 万 token 区间),我用的是本地模拟算的传输量,两者不是同一个指标,不能互相印证。

真实的延迟收益还跟网络状况、服务端负载有关。我这里没有 API 使用权,没法做端到端实测。这台机器上跑出来的只有传输量,不是延迟。

另外这套机制本质上把"会话"变成了一种服务端资源。以后如果多家厂商都这么做,会话的可迁移性会是个新问题——你换一家供应商,历史会话带不走。

结语

这个功能的定位很清楚:给长跑型 Agent 降延迟。

如果你的任务动辄几十轮、上下文几十万 token,它值。如果你的调用都是独立的短请求,它对你没有意义。

判断标准就一条:看你的请求体里有多少内容是上一轮已经发过的。 占比高,就有收益;占比低,就别折腾。

参考资料

目录导航
  • 一、message threads 解决的是什么:重复传输
  • 二、threads 的做法:会话只发一次
  • 三、我按 20 轮算了一遍会话传输量
  • 四、省的不是 token,是预填充时间
  • 五、threads 接口大概长什么样
  • 六、四个坑
  • 七、边界
  • 结语
    • 参考资料
  • 相关文章:

  • 上下文压缩三种策略实测:省到 100 token,就得接受会漏
  • 腾讯混元 Hyra 2.0:三层 Agent 做科研,从解题变成造工具
  • DeepSeek 的 Creator Mode:说一句话,生成一个能用的插件
  • Agent 开始有身份了:内容站该怎么声明谁能来、能干什么
  • Agent 的算力账单:一个任务跑到 1333 步,整月订阅就没了
  • Kimi 连干 20 小时,SlopCodeBench 无一通过:验收口径不同
  • Anthropic 招股书:Q3 营收近 170 亿,单季却亏 233 亿
  • AI 用 53 分钟打穿内核:漏洞研究的四步,它赢在第二步和第三步
  • 发表评论

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。