Claude API Mechanics
一次调用的解剖,与 realtime / batch 之间那条线
blueprint 把 messages、tools、streaming、vision、thinking、caching、经第三方 vendor 调用、Messages API data access patterns、batch API 使用、realtime 与 batch 的选型取舍,一并压进了同一条 skill。这些名词看起来是十个话题,实际只有两个问题:一次请求里数据怎么进、结果怎么出,以及这份工作该走同步那条路还是批处理那条路。本章按这两个问题组织,核心交付是第七节的对照表。
vb: 出处(登记表见 reference/verified-behaviors.md,含官方原句与查证日期)。考纲只点了这些机制的名字、没给行为定义,所以「名词在考纲里」与「行为细节可写」是两件事——这正是 ch01 P5 的边界警告对本书自己的要求。一、一次 Messages API 调用的解剖
先把一次调用拆成七个零件:请求侧四个、响应侧三个。后面所有机制都是往这七个零件上挂东西。
请求侧:model、max_tokens、顶层 system、messages 数组。这里有一条最常被写错的结构约束,而且它比多数人以为的更细——问的不是「system 能不能进 messages」,是「这条指令要从什么时候开始生效」。
从一开始就要生效的全局指令,走顶层 system 字段。API 参考页的说法是:想加 system prompt 就用顶层 system 参数;而 messages 的第一条不能是带内容的 system message。从别家 SDK 迁过来的代码最容易在这里翻车——把一条 {"role": "system", …} 摆在 messages[0],那不是「system prompt 生效了」,而是把它放错了位置。
对话中途才需要生效的指令,可以用 mid-conversation system message。在 Fable 5.1 / Mythos 5.1 / Fable 5 / Mythos 5 / Opus 5.5 / Opus 4.8 / Opus 5 / Sonnet 5.5 / Haiku 5.5 上(以官方列表为准),你可以在一个 user turn 之后放 "role": "system" 的消息,在对话进行到一半时追加新的 system 指令(受 placement rules 约束,仍然不能是第一条)。它的权威性不打折——官方明说 mid-conversation system message 与顶层 system 字段同等权威。
messages[0] 不能是 system」与「messages 里放 system 一定报错」是两句完全不同的话,只有前一句成立。做题时要抓的判据是指令的生效时点:从头生效 → 顶层 system;中途生效 → mid-conversation system message。messages 本身是回合序列,不是消息列表。模型被训练成在 user 与 assistant 交替的对话回合上工作;如果你在请求里连着放两条同 role 的消息,它们会被合并成一轮,而不是被当成两轮。工程含义:想让模型「看见两段独立的用户输入」,靠连发两条 user 消息是做不到的,得靠一条消息里的两个 content block,或者显式的分隔标注。
响应侧:content(content block 数组)、stop_reason、usage。
| 响应字段 | 它回答的问题 | 集成层该拿它做什么 |
|---|---|---|
content | 模型产出了哪些块(text / thinking / tool_use / …) | 按 type 分派,不假定只有一个 text 块 |
stop_reason | 为什么停下来 | 先读它再读 content:tool_use 表示要跑工具,max_tokens 表示输出被截断 |
usage | 这次调用记了多少 token | 成本与预算的唯一权威读数 |
stop_reason 有两个容易踩的性质。其一,取值 "max_tokens" 的官方释义是「触到了请求的 max_tokens 或模型上限」——也就是说,一个看起来完整的 JSON 响应,如果 stop_reason 是 max_tokens,那它极可能是被切断的半截。其二,非流式响应里这个字段恒非空;流式下它在 message_start 事件里是 null,其余情况非空。所以「流式收完第一帧就判断 stop_reason」的代码,永远读到 null。
usage 的算法也不是直觉的那样。官方定义:一次请求的总输入 token 等于 input_tokens、cache_creation_input_tokens、cache_read_input_tokens 三项之和。只读 input_tokens 做成本报表的团队,会在开启缓存之后突然「发现输入 token 大幅下降」——那不是省了,是漏读了两个字段。
二、Messages API data access patterns:无状态是它最重要的性质
blueprint 单独点了「Messages API data access patterns」这个短语。要抓的第一件事不是某个字段,而是这个 API 的状态模型。
官方对它的定位是:可用于单次查询,或者无状态的多轮对话。「无状态」这三个字决定了一整类工程后果——服务端不替你记住上一轮,会话历史是你每次重新提交的。于是:
- 对话越长,每次请求的输入越大,成本与延迟随轮数单调上升;
- 任何「让它记住上次说过的话」的诉求,落点都在你的存储层,不在 API;
- 你放进历史里的每一个字节,都会被重发一次——这正是第五节 vision 那笔账的来源。
数据进模型的通道有三条,都在 messages[].content 这一层:
| 通道 | 形态 | 典型用途与代价 |
|---|---|---|
| 内联内容块 | text / image / document 块直接写进请求体 | 最简单;体积算进每一轮的输入 |
| 引用型来源 | URL 或 Files API 的 file_id | 上传一次、多次引用;payload 不随轮数膨胀 |
| 工具回传 | tool_result 块,作为 user 回合送回 | 活数据只能靠调用去取(ch01 P4) |
还有一个历史上常用、现在已经不能再用的输出侧模式:prefill。API 参考页至今仍写着它的通用行为——最后一条消息用 assistant role 时,响应会直接从那条内容往下续写,可以用来约束模型回复的一部分。很多老代码正是靠它把输出钉成 JSON 的。
但在当前的主力模型上,这么写会直接失败。从 Claude 4.6 各模型与 Claude Mythos Preview 起,最后一条 assistant turn 上的 prefill 不再被支持,带 prefill 的请求返回 400。两条边界要一起记:更早的模型仍然支持 prefill;而在对话的其他位置添加 assistant 消息不受影响——所以「把那段半截 JSON 挪到倒数第二条」既不会报错,也不再起约束作用。官方给的理由是模型的智能与指令遵循已经进步到多数 prefill 用例不再需要它。
迁移到哪儿去:官方给了一整套替代路径,它们的完整取舍由 ch10 第四节主讲,本章不重复。本章要你带走的只有上一段那条硬事实——在 4.6 及之后的模型上,最后一条 assistant turn 的 prefill 返回 400,所以读到任何「用 prefill 把输出钉成 JSON」的老代码或老选项,先按这条判。
为什么这一段值得留着而不是删掉:它是本书的一个方法论样本。/en/api/messages 这类 API 参考页的原句是真的,但它给的结论可能被更专门的页面推翻——system role 与 prefill 已经各踩过一次。凡是从 API 参考页登记的行为,落笔前先找同主题的专题页;冲突时以专题页为准。
另一层连接:即使换成 Structured Outputs,它保证的也只是形状而不是取值——模型仍可能在合法 JSON 里给出错误内容,所以出口处的 parse + validate 一步不能省(ch01 P7)。完整的出口纪律见 ch10 第五节。这一段是本书的连接推论,不在 guide 措辞里。
三、tools 与 thinking:把「一次调用」撑成「一轮往返」的两个机制
tools。客户端工具的往返是固定三拍:模型返回 stop_reason: "tool_use" 与一个或多个 tool_use 块 → 你的代码执行那个操作 → 你把结果作为 tool_result 块送回去。注意第三拍的方向:tool_result 是你回传给 API 的内容,要随着下一次请求重新进入 messages,它不是「响应的一部分」。工具的设计与描述写法在 ch12 展开,这里只需要记住这个往返的形状,因为下面 thinking 的约束就挂在它上面。
thinking。up-front thinking(先想再答)的响应里,一个或多个 thinking content block 会排在 text 块之前。这带来两条硬约束,而且都是集成层的坑:
- 回传 tool result 时,assistant 消息里的 thinking block 要原样完整传回,与它伴随的
tool_use块一起。改动过的 thinking block 会被以400拒绝。为了「省 token」把 thinking 剥掉再回传,是这一型最常见的自伤。 - 在 manual extended thinking(
thinking: {type: "enabled"})下,tool use 只支持tool_choice为auto或none;写成any或指定某个 tool 会直接报错,因为这两种写法强制用工具,与 manual extended thinking 不兼容。
还有一条跨机制的联动值得先记下:切换 thinking 模式会让 prompt cache 失效。也就是说「这一轮开思考、下一轮关掉省钱」的做法,代价不止是那一轮的 thinking token。
四、streaming:事件序列,以及什么时候不流就会出事
流式响应走 server-sent events。官方对事件形态的描述是:每个事件带一个 SSE 事件名(例如 event: message_stop),并在它的 data 里带上同名的 type;每个内容块由一个 content_block_start、一个或多个 content_block_delta、一个 content_block_stop 包住。整条流的骨架是:
streaming · message_start ✔ 真实存在 · 流的开头
streaming · content_block_start ✔ 真实存在 · 内容块开始
streaming · content_block_delta ✔ 真实存在 · 可重复多次
streaming · content_block_stop ✔ 真实存在 · 内容块结束
streaming · message_delta ✔ 真实存在 · 顶层变更与累计 usage
streaming · message_stop ✔ 真实存在 · 流的结尾
streaming · content_block_flush ✘ 不存在 · 自制干扰项
两个专门用来出题的细节。第一,message_delta 事件 usage 字段里的 token 计数是累计值,不是增量——把每个 message_delta 的 output_tokens 加起来做账,会得到一个远大于真实值的数字。第二,thinking 内容在 content_block_stop 之前会先发一个 signature_delta 事件,这个 signature 用来校验 thinking block 的完整性;上一节「原样回传」的要求,在流式侧的落点就是它。
「什么时候用流式」不是体验问题,是可靠性问题。官方对长请求的建议是:超过 10 minutes 的长任务,考虑改用 streaming Messages API 或 Message Batches API;并且 SDK 会校验非流式的 Messages API 请求不应超过 10-minute timeout。更根本的差别在网络假设上——Batches 靠轮询取结果,因此不需要一条不中断的连接。
把三条路径并排:同步调用押注「一条连接撑到底」,流式让数据在这条连接上持续到达,批处理干脆不要长连接。选哪条,取决于你的工作能不能等,以及你的网络能不能撑。
五、vision:三种 source type,选的是成本形态
图片以 image content block 传入,官方给了三种 source type:请求体里内嵌的 base64、指向线上图片的 URL、Files API 返回的 file_id(上传一次、多次引用)。三者在模型侧看到的是同一张图,差别全在数据怎么进来。
| source type | 数据在哪 | 多轮里的行为 | 第三方 vendor 上 |
|---|---|---|---|
base64 | 请求体内 | 每轮随历史重发全部字节 | 可用 |
url | 你托管的地址 | 请求体只带 URL | 不可用 |
file_id | Files API | 上传一次,后续按 id 引用 | 不可用 |
第二节那句「无状态」在这里收账。官方的描述很直白:多轮与 agentic 流程里每次请求都重发完整对话历史;如果图片是 base64 编码的,完整的图片字节会随每一轮进入 payload,随着对话增长显著抬高请求体积与延迟。所以在一个走十几轮的 agent 里,把截图换成 file_id 引用,改的不是画质而是请求体积的增长曲线。
另一条要提前记住:在 Amazon Bedrock 与 Google Cloud 上,目前只有 base64 source 可用。这条会在第八节和本章第 7 题再出现一次——同一段能跑通的代码,换个接入点就未必。
六、caching:前缀有层级,失效也有层级
blueprint 在 TS 2.3 只写了 caching 这个词。缓存的经济学(省多少、什么时候值得)在 ch07;本章只讲它在 API 结构上怎么生效、怎么失效——因为这一半才是集成 bug 的来源。
官方定义的前缀顺序是 tools → system → messages,后一层建立在前一层之上;并且某一层的改动会让该层及其之后的所有层失效。层内还有一条同样硬的规则:缓存条目的 hash 是累积的,覆盖到断点为止的全部前缀,所以断点处或断点之前的任何一块只要变了,下一次请求就是另一个 hash;带变动后缀(时间戳、每请求上下文、这一轮的来信)的 prompt,断点该放在静态前缀的末尾,而不是放在那个每次都变的块上。把这句话翻译成排查动作:缓存命中率突然掉到零,先问「今天有没有人动过 tool 定义」——改一个工具的 description,整条链上的缓存一起作废。
第二个坑更隐蔽。短于最小可缓存长度的 prompt不会被缓存,而且不返回任何错误——官方原话是「will be processed without caching, and no error is returned」。你的日志里不会有任何异常,账单上也没有明显信号。验证办法只有一个:看 usage 里的 cache_creation_input_tokens 与 cache_read_input_tokens,两个都是 0 就说明这次没缓存上。
剩下三条各占一行:缓存默认 5-minute lifetime,每次被用到都会免费刷新一次;一个缓存条目要等第一个响应开始之后才可用,所以并发打出去的首批请求全部落空,官方建议是先等第一个响应回来再发后续请求;切换 thinking 模式会让缓存失效。
把这个层级和第一节的两种 system 指令放在一起看,会得到一个很实用的工程取舍。长会话跑到一半才想加一条指令,改顶层 system 是最贵的做法——system 在前缀链的第二层,动它会让该层及其之后的 messages 层一起失效,整段几万 token 的前缀要重写一次。mid-conversation system message 走的是另一条路:它追加在消息历史的末尾,因此不会让它之前的 cached prefix 失效。既然它与顶层 system 同等权威,「中途加指令」这件事就有了一个不炸缓存的做法。
七、batch vs. realtime:本章的核心对照表
这是 TS 2.3 里唯一被写成「取舍」的条目,也是官方样题 Q1 的考点。先把两条路径逐轴摊开,再谈怎么选。
| 判断轴 | realtime(同步 / 流式 Messages API) | Message Batches API |
|---|---|---|
| 结果何时可得 | 本次调用内返回 | 多数批次 1 小时内完成;结果在「全部完成」或「满 24 小时」先到者时可取;24 小时内没跑完即过期 |
| 提交与取回 | 一次 HTTP 往返,或一条 SSE 流 | POST /v1/messages/batches 提交,创建后立即开始处理;processing_status 由 in_progress 转 ended 后结果就绪 |
| 结果形态 | 响应体 / 事件流 | .jsonl,每行是一个请求的结果 |
| 结果与请求的对应 | 天然一对一 | 靠 custom_id 匹配,结果可能不按提交顺序给出 |
| 网络假设 | 需要一条不中断的连接 | 轮询取结果,不依赖长连接 |
| 失败爆炸半径 | 本次调用失败即本次失败 | 批内单个请求失败不影响其他请求 |
| 参数面 | 完整 | 少数参数不支持,带上直接 validation error;stream: true 即其一,因为批结果是一个文件而不是流 |
| 规模上限 | 受 rate limit 约束 | 单批 100,000 条请求或 256 MB,先触到哪个算哪个 |
| 与 caching 的关系 | cache_control 按第六节的规则生效 | 支持,但异步并发处理使命中是尽力而为 |
| 计费 | 标准价 | 全部用量按标准 API 价格的 50% 计 |
| 第三方 vendor | Bedrock / Google Cloud 上可用 | Google Cloud 与 Bedrock 两页的 Features not supported 小节都列着它 |
vb:vb-batch-price-o28)。guide 本身没有给出折扣数字。考场上遇到只写 "at reduced cost" 的选项,它并不因为没写百分比而变弱。怎么选:三个问题就够
- 调用方能不能等?有人正盯着屏幕等结果 → realtime。次晨要、报表要、离线评测要 → batch。这一问已经决定了大多数题。
- 量有多大、优化的是什么?题干把「成本」写成首要约束、把体量写成几万条时,batch 是那条同时通过两道筛子的路径(ch01 P1)。
- 集成层扛不扛得住?需要长连接的场合(严格的网络策略、不稳的出口、超过 10 minutes 的单次生成),batch 的轮询模型本身就是可靠性优势。
官方样题 Q1 精讲
Q1(Domain 2 / TS 2.3)给的场景是:过夜处理一万份文档、成本是首要考虑、次晨才要结果。正确项是 Message Batches API,guide 的措辞是 "processes large asynchronous workloads within a 24-hour window at reduced cost"。真正值钱的是它的三条排除理由,每一条都是一台可复用的裁决机器:
- 「同步 + 并发跑」——官方判词是它 "does not reduce per-token cost"。并发压的是 wall clock,题干指认的变量是成本。这是 ch01 D2 无关杠杆 的标准形态。
- 「压低
max_tokens」——官方判词是它 "does not address the batch-versus-realtime tradeoff"。省钱这半句是真的,可它没碰题干那条轴,属 D6 半对半错。 - 「换个更小的模型」——题干没有一条约束说模型能力过剩,属 D4 换模型当解法。
三条合起来就是一句读题动作:先认出题干指认的那个变量,再看每个选项作用在哪个变量上。
参数真伪速查(batch 相关)
batch API · custom_id ✔ 真实存在 · 结果回匹请求用的标识
batch API · processing_status ✔ 真实存在 · 批次状态字段
batch API · results_url ✔ 真实存在 · 结果下载地址
prompt caching · cache_control ✔ 真实存在 · 显式缓存断点
batch API · batch_priority ✘ 不存在 · 自制干扰项
batch API · sequence_index ✘ 不存在 · 自制干扰项
下面两个捏造物挑得很有针对性:batch_priority 迎合「我想让某几条先跑」的直觉,sequence_index 迎合「结果总该有个顺序号吧」的直觉——而后者恰恰与 custom_id 存在的理由相反。看到没见过的字段,先按不存在处理(ch01 D1 / P5)。
八、经第三方 vendor 调用 Claude:差异清单
blueprint 把「经第三方 vendor 调用 Claude」写进了 TS 2.3。考点不是某家云的控制台怎么点,而是同一段代码换个接入点之后,哪些东西不再成立。
| 差异面 | 直连 Claude API | 经第三方 vendor |
|---|---|---|
| 请求格式 | model 在 body,版本走 header | Google Cloud Agent Platform 上 model 写在端点 URL 里,anthropic_version 写在 body 里 |
| 图片来源 | base64 / URL / file_id 三选一 | Bedrock 与 Google Cloud 上目前只有 base64 |
| 可用端点 | 含 Message Batches | Google Cloud 与 Bedrock 两页的 Features not supported 小节都列着 Message Batches 在内的一组 API endpoints |
这张表最该带走的判断是第三行。「用 batch 省钱」这个结论,是绑定接入点的。一个在直连 API 上完全正确的架构决定,搬到 Bedrock 或 Google Cloud 上会变成一个 404——而这类题的干扰项通常写成「调大超时」「拆小批量」「换个 region 重试」,全是在一个根本不存在的端点上做优化(ch01 D2)。
本章刻意没写各 vendor 的鉴权方式、region 选择、model id 前缀规则:它们随平台演进的速度远快于一本备考书的修订周期,而 blueprint 的措辞停在「经第三方 vendor 调用 Claude」这一层。考场上真正会被问的,是差异存在且要在设计时被考虑,不是某个前缀今天长什么样。
本章题组
requests[].params still carries "stream": true. The submission is rejected with a validation error before anything is processed, and the console shows no batch at all. The nightly window is six hours and the job must not become interactive. What should the team change?stream: true,带上它会返回 validation error——因为批结果是一个文件,不是一条流。正确的取回方式是轮询到 processing_status 变为 ended,再读 .jsonl 结果。题干的两个约束(六小时窗口、不得变成交互式)都由这条路径满足。stream: true 一个字都没碰,换完模型仍然提交不上去。request_counts reporting 4,000 succeeded and 0 errored. The ingestion script reads the results file line by line and writes result n onto the invoice at index n of the original submission array. Spot checks find that about 12% of invoices now carry another invoice's totals. What is wrong?custom_id 的定义就是为这件事存在的:它用来把结果匹配回请求,因为结果可能不按请求顺序给出。按下标 zip 在小批量上可能一直「碰巧对」,规模一大就开始错位——12% 这个数字正是这种错位的典型形态,而不是失败率。request_counts 报 4,000 succeeded、0 errored,说明批次已结束且结果齐全。「快照不全」会表现为行数缺失,而现象是行数够、内容对错了位。这个选项去修一个数据已经证明运转正常的组件。sequence_index 是本书自制的干扰项,没有官方出处。它的可信度来自「结果总该有个顺序号」的直觉,而这个直觉与 custom_id 存在的理由正好相反——如果顺序是可靠的,官方就不必特意说明结果可能乱序。没见过的字段先按不存在处理。custom_id 匹配)连试都没试过,而且这个方案还顺手丢掉了单批的规模优势。system and marks the last system block with cache_control. That block ends with the line Generated at 2026-08-22T09:14:03Z, regenerated on every request. After a week in production, cache_read_input_tokens is 0 on every call, cache_creation_input_tokens is non-zero on every call, and nothing has ever been logged as an error. What explains it?cache_control 放在跨请求完全相同的那个块上,把变动后缀留在断点之后。cache_status 是本书自制的干扰项,没有官方出处。它之所以像真的,是因为它填上了一个真实存在的空白——低于最小长度时确实不返回任何错误。但官方给的验证手段是读 usage 的两个 cache 字段,而题干已经告诉你 write 非零,说明长度门槛早就过了。thinking blocks out of the assistant message before echoing it back together with the tool_result. The first leg succeeds and the tool returns valid JSON, but every second-leg request now fails with a 400 before the model is reached. Which change is correct?tool_use 块一起;改动过的 thinking block 会被以 400 拒绝。「剥掉 thinking 省 token」正是本型最常见的自伤——省下的那点输入 token 换来了整条第二腿不可用。tool_choice 是真实存在的机制,但这里被安上了它职责之外的能力——在 manual extended thinking 下,tool use 只支持 auto 或 none,指定具体 tool 会直接报错,因为那是强制用工具。名字真、职责假,是 D1 更难认的那个子型。usage.output_tokens from every message_delta event into a running total. Finance reports the dashboard bills roughly nine times what the console shows for the same traffic. The product has a strict first-token latency budget, so dropping streaming is not acceptable. What is the fix?message_delta 的 usage 里的 token 计数是累计值,把每一帧相加等于把同一批 token 数了很多遍——九倍这个量级正是「事件数量」而不是「token 数量」的影子。改法是记录最后一次读到的值——流的事件序列以一个 message_stop 收口,所以「最后一次」有明确的边界。顺带一提(不是本题判据):这个看板的输入侧同样会算错——一次请求的总输入 token 是 input_tokens、cache_creation_input_tokens、cache_read_input_tokens 三项之和,只读第一项会在开启缓存后凭空「省」出一大截。usage,且它明确包含缓存相关的两项——自制换算连这两项的存在都表达不了。message_start 确实带 usage,这半句是真的;但它是流开始时的快照,输出还没生成完,不可能是「整个响应的权威总数」。同一个事件里 stop_reason 也还是 null——把开场快照当终局读数,是给真实机制安错了职责。image block, and the client resends the whole conversation every turn. By turn 12 the request body is 38 MB, p95 latency has tripled, and per-turn input tokens keep climbing. Earlier screenshots are still referenced by later turns. Which change addresses the growth?file_id 正是「上传一次、多次引用」那一种,它改的是增长曲线本身。/v1/messages/batches returns 404. The nightly job processes 60,000 records inside a ten-hour window. What is the accurate reading of these two failures?model 写在端点 URL 里而不是 body,anthropic_version 则写在 body 里而不是 header。端点可用性上,Message Batches 被列在该平台的 Features not supported 小节里(Bedrock 页有同样一条)——404 不是配置问题,是这条路在这里不存在。anthropic_version 出现在 body 里。选项还额外编了一条「版本协商成功后端点就会解析」的因果,一个真实存在的 header 被安上了开通端点的能力。max_tokens 在官方样题 Q1 里被排除,用的正是这个理由:省钱是真的,跑错轴也是真的。