SDK 的输出流里只有四种消息:system、assistant、user、result。 认清这四种,就知道该在哪种上做统计。 最值钱的是 result:轮数、用量、花费全在那一条里。要算钱只读它,别自己去累加。
上一节会调 query 了,这一节看它吐出来的东西。 这一节真跑:六条消息按类型分流,再从 result 里把账算出来。
一、四种类型的分工
system / init:会话刚开始。带会话标识、可用工具列表、模型名。 assistant:模型的响应。要么是文本,要么是要调的工具与参数。 user:工具执行的结果。它被回喂给模型,所以也算「输入」。
result:任务结束。带轮数、用量、花费。 四类之外没有别的。看到不认识的类型,正确做法是跳过,不是报错。
二、真跑:六条消息按类型分流
演示里六条:init 一条、assistant 两条、user 两条、result 一条。 assistant 和 user 各两条,是因为一次工具调用会占两条:一条「要调」,一条「调完」。 这个对应关系很实用:数一下 tool_use 和 tool_result,就知道它调了几次工具。
而轮数不等于这个数——一轮里可能调十次工具。
三、result 那一条最值钱
轮数、输入 token、输出 token、花费全在里面。 所以要算钱,只读 result,不用自己去累加各条消息——累加容易漏(比如系统提示不计入消息)。 init 那一条最有用的是工具列表:能确认它到底拿到了哪些工具。
这两条合起来,等于「这次跑了多久、花了多少、用了什么」,一次拿全。
四、按类型分流的写法
一个 switch 或者 match,四个分支,加一个默认分支。 默认分支必须是「跳过」而不是「抛异常」——新版本加类型时你的程序不该崩。 这一条和 7.6 节解析事件流那个坑是同一个:别假设类型是固定的。
写完分流之后,建议在默认分支里打一行日志,这样新类型出现时你能发现。
五、一个常见的统计错误
把 assistant 的条数当成「轮数」。一轮里可能有多条 assistant(多次工具调用)。 轮数以 result 里的 num_turns 为准——那是它自己数的,比你数得准。 同理,「调了几次工具」也不要靠数消息,要看 tool_use 的条数。
总之一条原则:能用它给的汇总数,就别自己数。
六、动手 Demo:六条消息,四种类型,各自带什么
SDK 的输出流里只有四种消息:system、assistant、user、result。 这里把六条消息按类型分流,再从 result 那一条里把账算出来。
for m in MSGS:
cnt[m[u"type"]] = cnt.get(m[u"type"], 0) + 1
res = [m for m in MSGS if m[u"type"] == u"result"][0]
print(res[u"num_turns"], res[u"usage"], res[u"total_cost_usd"])
实跑输出:
--------------------------------------------------------------------
六条消息,四种类型,各自带什么
--------------------------------------------------------------------
SDK 的输出流里只有四种消息:system、assistant、user、result。
认清这四种,就知道该在哪一种上做统计、在哪一种上做展示。
① 四种类型的分工
--------------------------------------------------------------------
| 类型 | 什么时候出现 | 带什么 |
|---|---|---|
| system / init | 会话刚开始 | 会话标识、可用工具、模型名 |
| assistant | 模型的响应 | 文本、或要调的工具与参数 |
| user | 工具执行的结果 | 工具返回的内容,回喂给模型 |
| result | 任务结束 | 轮数、花费、用量 |
② 真跑:六条消息按类型分流
--------------------------------------------------------------------
| 类型 | 条数 | 占比 |
|---|:---:|:---:|
| assistant | 2 | 33% |
| user | 2 | 33% |
| system | 1 | 17% |
| result | 1 | 17% |
六条里 assistant 和 user 各两条——因为一次工具调用会占两条:一条「要调」,一条「调完」。
③ result 那一条最值钱
--------------------------------------------------------------------
| 字段 | 值 |
|---|---:|
| 轮数 | 4 |
| 输入 token | 3120 |
| 输出 token | 460 |
| 花费 | 0.0412 美元 |
轮数、用量、花费全在这一条里。所以要算钱,只读 result,不用自己去累加。
而 init 那一条最有用的是 tools 列表:能确认它到底拿到了哪些工具。
④ 按类型分流的写法
--------------------------------------------------------------------
[教材原样 · 本沙箱不执行]
| match message.type:
| case "system": ...
| case "assistant": ...
| case "user": ...
| case "result": ...
一定要有「不认识就跳过」的分支。新版本加一种类型时,你的程序不该崩。
⑤ 一个常见的统计错误
--------------------------------------------------------------------
把 assistant 的条数当成「轮数」。一轮里可能有多条 assistant(多次工具调用)。
轮数以 result 里的 num_turns 为准——那是它自己数的。
! 消息字段以真实版本为准,这里演示的是四种类型的职责划分。
①四类:system/init 带会话标识与工具列表、assistant 带文本或工具调用、user 带工具结果、 result 带轮数与花费。 ②六条里 assistant 和 user 各两条,因为一次工具调用占两条:一条「要调」、一条「调完」。 数一下 tool_use 就知道调了几次工具。 ③result 那一条最值钱:轮数、输入输出 token、花费全在里面。 要算钱只读它,别自己去累加——累加容易漏掉系统提示那部分。 ④分流要有一个「不认识就跳过」的默认分支。新版本加类型时,你的程序不该崩。
30 秒实验
- 再插一条 assistant(多一次工具调用),重跑一遍。assistant 和 user 的条数会一起变吗? 这说明了什么对应关系?
- 把 result 里的 num_turns 改成 1,重跑一遍。你的统计会跟着变吗?如果不会,说明你数的是什么?
- 插一条类型叫 progress 的消息,重跑一遍。分流会不会崩?默认分支该怎么写才安全?
常见误区
- 把 assistant 的条数当成轮数。一轮里可能有多条 assistant。
- 自己累加各条消息算花费。容易漏,而且不如 result 里的汇总准。
- 分流没有默认分支。新版本加一种类型,程序直接崩。
- 不认识的消息类型就抛异常。正确做法是跳过并记一行日志。
- 以为 init 只带版本号。它的工具列表是「确认它拿到了什么权限」的唯一依据。
自测三问(点开对答案)
1. 四种消息类型分别是什么? —— system(初始化,带会话标识与工具)、assistant(响应或工具调用)、user(工具结果)、 result(结束,带轮数与花费)。 2. 为什么一次工具调用会占两条消息? —— 一条 assistant 说「要调这个工具」,一条 user 带回「工具的结果」。 3. 算花费该读哪一条? —— result。轮数、输入输出 token、花费都在里面,比自己去累加准。流会长什么样清楚了,下一节学怎么调它的行为:选项体系。
- ☐ (本篇完,下一篇 8.4 ClaudeAgentOptions:精细的行为控制)
相关笔记
- 流式输出:流式解析的三个坑
- ClaudeAgentOptions:选项怎么配
- Claude Code 橙皮书 · 索引
- 🐎 Claude Code 实战 · 索引