Agent SDK 的五大消息类型(讲清 query 流的语义分工)

用 Claude Agent SDK 跑一次 query(),底层流回来的是结构化消息流,承载它的是 user、assistant、system、tool_result 与 result 五种类型;每种在循环里扮演不同角色——输入回声、模型回复、环境配置、工具回执、终止信号。把这五类认清,所有状态机分支、UI 进度展示、计费统计就有了落点。

一、消息流的整体形状

query() 返回一个 AsyncGenerator<SDKMessage>,调用方用 for await 不断 yield 消息。官方将 SDKMessage 定义为带 type 判别字段的联合类型,五种 type 构成一次完整循环的语义骨架。

消息类型 触发时机 携带核心字段 循环中的角色
system 会话初始化、插件加载、配置变更 session_id、tools、model、cwd 描述当前运行环境的元信息
user 应用层推入提示或多轮追问 message.content(文本或多模态) 把外部输入送进模型或回显到流
assistant 模型生成的回复 content[](text / tool_use / thinking) 模型表达思考、文本与工具意图
tool_result 工具执行完成后回传 tool_use_id、content、is_error 把工具输出反馈给模型继续推理
result 整轮或整次调用结束 subtype、total_cost_usd、usage 终止信号 + 成本/统计/错误摘要

stream_event 是增量分片,不在主循环骨架里,与上面五种属于”半步”粒度消息。理解这五类是看懂状态机的前提。

二、五种类型的语义角色

把循环压扁成”谁说话、谁干活、谁收尾”,每个角色都能落到具体消息上。

2.1 system:环境契约

system 消息在 query() 启动、插件加载、权限模式切换时触发,常见 subtype 是 init。它的核心作用是让调用方知道”接下来跑在什么环境里”:当前 session_id、可用 tools 列表、model 名称、cwd、permissionMode。前端拿这条消息做就绪态展示,后端用它做审计锚点。

{
  "type": "system",
  "subtype": "init",
  "session_id": "9c5b4c12-...",
  "tools": ["Read", "Edit", "Bash", "Glob", "Grep"],
  "model": "claude-sonnet-4-6",
  "cwd": "/repo/code-review",
  "permissionMode": "acceptEdits"
}

UI 接住它就显示”已连接,会话 id 9c5b4…”,这条信息不需要再发回模型,是纯环境描述。

2.2 user:输入回声

应用层把提示词以 prompt 字符串或 AsyncIterable<SDKUserMessage> 形式交给 query(),循环会原样回放一条 user 消息。它的语义是”这是用户说的”,不是工具结果,也不是模型自己想起的补充;调试时看到 user 就知道是入口。user 消息的 content 支持文本、图片等多模态块,多轮场景下还可以中途异步 yield 一条新 user 消息来打断长任务。

2.3 assistant:模型输出

assistant 是循环里最常出现的消息类型,content 是一个 ContentBlock[],三种块按需组合:

  • text:自然语言回复;
  • tool_use:模型决定调用某个工具,附 id、name、input;
  • thinking:扩展思考开关打开后附带的内部推理。

循环里它承担两件事:把”我打算做什么”用文本说给用户听;用 tool_use 表达”请外部替我执行这一步”。后者会触发下一条 tool_result,循环就靠这个来回推进。

{
  "type": "assistant",
  "message": {
    "role": "assistant",
    "content": [
      { "type": "thinking", "thinking": "先把目录列出来再读关键文件" },
      { "type": "text", "text": "我先看一下仓库结构" },
      { "type": "tool_use", "id": "toolu_01", "name": "Bash", "input": { "command": "ls -la" } }
    ]
  }
}

2.4 tool_result:执行回执

tool_result 不会主动产生——它在 tool_use 之后被 SDK 注入。核心字段是 tool_use_id,与上一条 assistant 里的 tool_use.id 严格对应,content 承载工具返回的字符串或结构化结果,is_error 标记失败。

语义上有两条要注意:

  • 工具失败仍属于正常循环信号,模型会读到 is_error=true 后决定重试、改方案或放弃;
  • tool_result 是循环”记忆”的一部分,模型下一轮决策要参考它,因此不要在 UI 层私自吞掉。
{
  "type": "user",
  "message": {
    "role": "user",
    "content": [
      {
        "type": "tool_result",
        "tool_use_id": "toolu_01",
        "content": "src  README.md  package.json",
        "is_error": false
      }
    ]
  }
}

注意 tool_result 实际上挂在 user 通道下,模拟”用户把工具结果转交给模型”——这是 Anthropic Messages API 的设计,循环里把它单列成一种类型更直观。

2.5 result:终止与统计

循环到 max_turns、end_turn、错误或用户主动停止时,最后一条消息是 result,subtype 通常是 success,错误场景下是 error_max_turns、error_during_execution 之类。它带 total_cost_usd、usage(输入/输出 token、缓存读写)、turn_count,是 UI 显示”完成 + 花了多少 + 转了几次”的唯一来源。

{
  "type": "result",
  "subtype": "success",
  "total_cost_usd": 0.0231,
  "usage": {
    "input_tokens": 412,
    "output_tokens": 612,
    "cache_read_input_tokens": 1024
  },
  "turn_count": 4
}

看到 result 就可以关掉进度条、扣减预算、写日志。

三、循环的状态机视角

把五种消息摆进时间轴,一次完整的 query() 大致长这样:

  1. 应用层 query({ prompt });
  2. SDK 立刻 yield system(init),给出会话配置;
  3. SDK 注入 user,回放提示词;
  4. 模型思考后 yield assistant(含 text 或 tool_use);
  5. 工具执行,SDK 注入 tool_result;
  6. 回到第 4 步,直到模型给出”无 tool_use 的 assistant”或达到 maxTurns;
  7. 末条 result(success) 收尾。

每一步都不是”SDK 在猜”,而是某一种消息被严格产生。循环天然支持 stream_event 增量分片,但它位于主流程之外,不打断状态机。

四、常见消费模式

4.1 抽取所有工具调用

import { query } from "@anthropic-ai/claude-agent-sdk";

function extractToolUses(messages: SDKMessage[]) {
  const calls: Array<{ id: string; name: string; input: unknown }> = [];
  for (const m of messages) {
    if (m.type !== "assistant") continue;
    for (const block of m.message.content) {
      if (block.type === "tool_use") {
        calls.push({ id: block.id, name: block.name, input: block.input });
      }
    }
  }
  return calls;
}

4.2 实时打印进度

for await (const m of query({ prompt: "重构支付模块" })) {
  if (m.type === "system" && m.subtype === "init") {
    console.log("会话", m.session_id, "工具数", m.tools.length);
  } else if (m.type === "assistant") {
    for (const b of m.message.content) {
      if (b.type === "text") console.log("Claude:", b.text);
      else if (b.type === "tool_use") console.log("→ 调用", b.name);
    }
  } else if (m.type === "result" && m.subtype === "success") {
    console.log("完成,花费", m.total_cost_usd, "轮次", m.turn_count);
  }
}

4.3 错误分流

result.subtype 是分流的关键:success 进入成功分支,error_max_turns 走”轮次耗尽”分支,error_during_execution 走”中途异常”分支,分别记录到不同告警通道。

五、循环之外的注意点

  • 启用 includePartialMessages: true 后会出现 stream_event 增量消息,用来驱动打字机式 UI;它不替代上面五类,只是把 assistant 内容拆细;
  • V2 API 通过 unstable_v2_createSession 维持多轮上下文,会话恢复时 SDK 仍按这五类消息回放,但由会话对象托管而非 query() 一次性 yield;
  • 权限拦截在 permissionMode: "plan" 或自定义 canUseTool 下会以 tool_result(is_error=true) 形式出现,循环不会断;
  • 计费只看 result.usage,不要拿 tool_result 的体积反推 token。

到这里,一次 query() 循环从初始化、用户回声、模型决策、工具回执到终止统计的语义就完整了;把这五类消息分别落到 UI、审计、计费三处,整套 SDK 编程就立得起来。

常见问题(FAQ)

Q1:为什么 tool_result 挂在 user 通道里?

沿用了 Anthropic Messages API 的角色设计:把”用户把工具输出转交模型”模拟为用户通道消息,循环里单独识别即可。

Q2:stream_event 算第六种消息类型吗?

不算主循环骨架,它是 assistant 内容的增量分片,启用 includePartialMessages 时才出现。

Q3:怎么判断循环是真的结束还是被中断?

只有 result 消息是终止信号,循环里见不到 result 就代表被 AbortController 打断,需要应用层补一次状态补偿。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
赵其鑫的头像赵其鑫管理团队

相关推荐

返回顶部