claude agent sdk 指南

02-流式输出

读完这篇你能:把回复改成流式接收,理解流式消息里多出来的标志和「块」。

前置:纯聊天。约 25 分钟。

需求:通过 agent sdk 实现聊天消息的流逝输出

消息

流式消息的结构与上一个例子不同了,消息开始增加一些标志,而且引入了块的概念。

Clipboard_Screenshot_1786673109

流式部分消息

type SDKPartialAssistantMessage = {
  type: "stream_event";
  event: BetaRawMessageStreamEvent; // 来自 Anthropic SDK
  parent_tool_use_id: string | null;
  uuid: UUID;
  session_id: string;
  ttft_ms?: number; // 首个令牌的时间(毫秒),仅在 message_start 事件上显示
};

上图中除了完整块和最后的结果消息,中间其他的都变成了stream_event消息。建议先自己整体看下完整消息流水。

上述结构在本例子中没有什么特别可介绍的,摘几个重要的节点。

完整块的结构就是非流式输出中的message。

结果消息与 第一个例子 也是相同的。

  • 消息开始标志

    {
      "type": "stream_event",
      "event": {
        "type": "message_start",
        "message": {
          "model": "claude-haiku-4-5-20251001",
          "id": "msg_bdrk_01N7yzbHnSkQBSGYnyDyGBDy",
          "type": "message",
          "role": "assistant",
          "content": [],
          "stop_reason": null,
          "stop_sequence": null,
          "usage": {
            "input_tokens": 10,
            "cache_creation_input_tokens": 0,
            "cache_read_input_tokens": 31828,
            "cache_creation": {
              "ephemeral_5m_input_tokens": 0,
              "ephemeral_1h_input_tokens": 0
            },
            "output_tokens": 7
          }
        }
      },
      "session_id": "c32b31c3-1abf-48db-abbc-eda31be92346",
      "parent_tool_use_id": null,
      "uuid": "8c162957-cb99-443a-9833-2c793f3ae0a3",
      "ttft_ms": 2681
    }
    
  • 块开始标志

    {
      "type": "stream_event",
      "event": {
        "type": "content_block_start",
        "index": 0,
        "content_block": {
          "type": "thinking",
          "thinking": "",
          "signature": ""
        }
      },
      "session_id": "c32b31c3-1abf-48db-abbc-eda31be92346",
      "parent_tool_use_id": null,
      "uuid": "d1905d8b-5298-4020-beda-3f662ea47a4b"
    }
    

    ` “index”: 0`这个是块的索引,本次消息共有两个块,一个思考块和一个回复块。思考块的索引是0

  • 块增量

    {
      "type": "stream_event",
      "event": {
        "type": "content_block_delta",
        "index": 0,
        "delta": {
          "type": "thinking_delta",
          "thinking": "The user is asking me to rec"
        }
      },
      "session_id": "c32b31c3-1abf-48db-abbc-eda31be92346",
      "parent_tool_use_id": null,
      "uuid": "4cfaa446-7f2c-4a30-9bc7-70b31f5a59aa"
    }
    
  • 块结束标志

    {
      "type": "stream_event",
      "event": {
        "type": "content_block_stop",
        "index": 0
      },
      "session_id": "c32b31c3-1abf-48db-abbc-eda31be92346",
      "parent_tool_use_id": null,
      "uuid": "eea7eda3-ae4b-4b51-bc58-7984dc9e8e53"
    }
    
  • 消息结束标志

    {
      "type": "stream_event",
      "event": {
        "type": "message_stop",
        "usage": {
          "input_tokens": 10,
          "output_tokens": 658,
          "cache_creation_input_tokens": 0,
          "cache_read_input_tokens": 0
        }
      },
      "session_id": "c32b31c3-1abf-48db-abbc-eda31be92346",
      "parent_tool_use_id": null,
      "uuid": "e9a109e8-af72-4eac-8fde-5f5853874677"
    }
    

API

调用示例

query({
    prompt: userMessage,
    options: {
      pathToClaudeCodeExecutable: '/opt/homebrew/bin/claude',
      model: 'claude-haiku-4-5-20251001',
      resume: sessionId,
      
      // 开启 partial 消息,才会收到 type: 'stream_event' 的增量事件
      includePartialMessages: true,
    },
  });

很简单,请求时加个配置就好 includePartialMessages: true

20260813130533_rec_

完整代码

import { query, type SDKMessage } from '@anthropic-ai/claude-agent-sdk';
import { appendFileSync, mkdirSync } from 'fs';
import { dirname, join } from 'path';
import { createInterface } from 'readline';
import { fileURLToPath } from 'url';

const rl = createInterface({ input: process.stdin, output: process.stdout });
const ask = (prompt: string): Promise<string> =>
  new Promise((resolve) => rl.question(prompt, resolve));

const LOG_DIR = join(dirname(fileURLToPath(import.meta.url)), 'logs');
const LOG_FILE = join(LOG_DIR, `sdk-messages-${new Date().toISOString().replace(/[:.]/g, '-')}.jsonl`);
function writeMsgToFile(msg: SDKMessage): void {
  mkdirSync(LOG_DIR, { recursive: true });
  appendFileSync(LOG_FILE, `${JSON.stringify(msg)}\n`, 'utf8');
}

async function sendMessage(userMessage: string, sessionId?: string): Promise<string | undefined> {
  const q = query({
    prompt: userMessage,
    options: {
      pathToClaudeCodeExecutable: '/opt/homebrew/bin/claude',
      model: 'claude-haiku-4-5-20251001',
      resume: sessionId,
      // 开启 partial 消息,才会收到 type: 'stream_event' 的增量事件
      includePartialMessages: true,
    },
  });

  let sid = sessionId;

  for await (const msg of q) {
    switch (msg.type) {
      case 'system': {
        break;
      }
      default: {
        writeMsgToFile(msg);
        break;
      }
    }
    switch (msg.type) {
      case 'stream_event': {
        // 真正的流式增量:content_block_delta + text_delta
        sid = msg.session_id;
        const event = msg.event;
        if (
          event.type === 'content_block_delta' &&
          event.delta.type === 'text_delta'
        ) {
          process.stdout.write(event.delta.text);
        }
        break;
      }
      case 'assistant': {
        // 完整消息会在流结束后再来一次;正文已由 stream_event 打出,这里只记 session
        sid = msg.session_id;
        break;
      }
      case 'result': {
        if (msg.is_error) {
          console.error(`\n[错误] ${msg.subtype}`);
        } else {
          process.stdout.write('\n');
        }
        break;
      }
      default:
        break;
    }
  }

  return sid;
}

export async function streamingOutput(): Promise<void> {
  console.log('====================================');
  console.log('  Claude Agent SDK 流式输出示例');
  console.log('  输入 "exit" 或按 Ctrl+C 退出');
  console.log('====================================\n');

  let sessionId: string | undefined;

  while (true) {
    let userInput: string;
    try {
      userInput = await ask('你: ');
    } catch {
      break;
    }

    userInput = userInput.trim();
    if (!userInput) continue;
    if (userInput.toLowerCase() === 'exit') break;

    process.stdout.write('\nClaude: ');

    try {
      sessionId = await sendMessage(userInput, sessionId);
    } catch (sdkErr) {
      console.error(`\n[SDK 错误] ${(sdkErr as Error).message}`);
    }

    console.log();
  }

  console.log('\n再见!');
  rl.close();
}


完整的事件流水有 3400 多行,单独放在附件里:流式消息完整流水。


下一篇:展示思考过程——既然能拆出增量,就能把 thinking 单独拎出来渲染。