🎏

[Anthropic API] Claude 3.7 Sonnet (Extended Thinking) のストリームイベント処理

2025/03/04に公開

先日 Claude 3.7 Sonnet がリリースされ、モデルの思考プロセスを出力する Extended Thinking 機能が追加されました。Anthropic API でも新たに thinking オプションが追加され、type: thinking で思考プロセスを出力できるようになりました。

この記事では、Stream での Extended Thinking の処理方法やハマりどころを解説します。

サンプルコードは、下記のリポジトリに置いています。動作を見たい方は下記をご覧ください。

https://github.com/HeavenOSK/sonnet-thinking-demo

概要

Anthropic API の Claude 3.7 Sonnet で Extended Thinking を使用する場合、type: texttype: tool_use のハンドリングに加えて type: thinking 用の処理が必要です。

ストリームで type: thinking をハンドリングする場合の要点は、以下の2点です。

  • content_block_start, content_block_delta に追加された、Extended Thinking 用の type を処理すること
  • 会話履歴に載せる際のフォーマットに気をつけること。具体的には type: thinking のコンテンツブロックに signature フィールドを含めること

以下では TypeScript で、@anthropic-ai/sdk を使用する前提で解説します。

https://www.npmjs.com/package/@anthropic-ai/sdk

Stream 処理全体

Stream 処理全体はこんな感じです。

// ストリーミングモードでAPIを呼び出し
const stream = await client.messages.stream({
  model: MODEL,
  messages,
  system: SYSTEM_PROMPT,
  max_tokens: 16000,
  temperature: 1.0, // Extended Thinking使用時は必ず1.0に設定する必要がある
  thinking: {
    type: 'enabled',
    budget_tokens: THINKING_BUDGET_TOKENS,
  },
});

// 完成したコンテンツブロックを保持する配列
const contentBlocks: StreamContentBlock[] = [];
let currentTextBlock: StreamTextBlock = { type: 'text', text: '' };
let currentThinkingBlock: StreamThinkingBlock | null = null;

// ストリームからのイベントを処理する部分
for await (const event of stream) {
  switch (event.type) {
    case 'content_block_start':
      if ('content_block' in event && event.content_block) {
        const contentBlock = event.content_block;
        switch (contentBlock.type) {
          case 'text':
            // 省略
            break;
          case 'thinking':  // 新しいタイプ:thinking
            currentThinkingBlock = {
              type: 'thinking',
              thinking: contentBlock.thinking,
              signature: contentBlock.signature 
            };
            break;
        }
      }
      break;
    case 'content_block_delta':
      switch (event.delta.type) {
        case 'text_delta':  // 通常のテキストデルタ
          // 省略
          break;
        case 'thinking_delta':  // 新しいタイプ:thinking_delta
          if (currentThinkingBlock) {
            currentThinkingBlock.thinking += event.delta.thinking;
          }
          break;
        case 'signature_delta':  // 新しいタイプ:signature_delta
          if (currentThinkingBlock) {
            currentThinkingBlock.signature = event.delta.signature;
          }
          break;
      }
      break;
    case 'content_block_stop':
      // 省略  
      break;
    case 'message_stop':
      // 省略  
      break;
  }
}

このコードでは、switch文を使ってイベントタイプに応じた処理を行っています。Extended Thinkingで追加された新しいタイプ処理のポイントは以下のとおりです:

1. thinking コンテンツのストリーム開始時の状態

ストリーム開始時の状態 (content_block_start) で注意したいのは、thinkingsignature が最初は空文字で流れてくることです。
type:thinking のコンテンツに signature が必須なのですが、最初この開始時に得られると勘違いしていたのでハマりました。

case 'thinking':  // 新しいタイプ
  currentThinkingBlock = {
    type: 'thinking',
    thinking: contentBlock.thinking, // この時点では空文字
    signature: contentBlock.signature  // この時点では空文字
  };
  break;

2. thinking_delta の処理

思考プロセスはここで流れてきます。

case 'thinking_delta':  // 新しいデルタタイプ
  if (currentThinkingBlock) {
    currentThinkingBlock.thinking += event.delta.thinking;
    handlers.onThinkingDelta?.(event.delta.thinking);  // コールバック実行
  }
  break;

3. signature_delta の処理

signaturesignature_delta で受け取ることができます。これが流れてくるのは一回だけです。

case 'signature_delta':  // 新しいデルタタイプ
  if (currentThinkingBlock) {
    currentThinkingBlock.signature = event.delta.signature;
  }
  break;

会話履歴に type:thinking のコンテンツを載せる際には、下記のように signature が必須なので保持しておく必要があります。

content: [
  {
    type: 'thinking',
    thinking: "思考内容...",
    signature: "Signature..."
  }
]

処理全体のコードを読みたい方は下記をご覧ください。

https://github.com/HeavenOSK/sonnet-thinking-demo/blob/main/src/services/api/anthropic-stream.ts

まとめ

という感じで、Claude 3.7 Sonnet (Extended Thinking) の Stream での処理を解説しました。今回のモデルリリースで、tool_use を含めたエージェントの性能がかなり上がりそうで非常に楽しいです。

今回は、@anthropic-ai/sdk を使用する前提で書いていますが、vercel/ai を使うのも手だと思います。

参考資料

Discussion