[Anthropic API] Claude 3.7 Sonnet (Extended Thinking) のストリームイベント処理
先日 Claude 3.7 Sonnet がリリースされ、モデルの思考プロセスを出力する Extended Thinking 機能が追加されました。Anthropic API でも新たに thinking オプションが追加され、type: thinking
で思考プロセスを出力できるようになりました。
この記事では、Stream での Extended Thinking の処理方法やハマりどころを解説します。
サンプルコードは、下記のリポジトリに置いています。動作を見たい方は下記をご覧ください。
概要
Anthropic API の Claude 3.7 Sonnet で Extended Thinking を使用する場合、type: text
や type: tool_use
のハンドリングに加えて type: thinking
用の処理が必要です。
ストリームで type: thinking
をハンドリングする場合の要点は、以下の2点です。
-
content_block_start
,content_block_delta
に追加された、Extended Thinking 用の type を処理すること - 会話履歴に載せる際のフォーマットに気をつけること。具体的には
type: thinking
のコンテンツブロックにsignature
フィールドを含めること
以下では TypeScript で、@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
) で注意したいのは、thinking
と signature
が最初は空文字で流れてくることです。
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 の処理
signature
は signature_delta
で受け取ることができます。これが流れてくるのは一回だけです。
case 'signature_delta': // 新しいデルタタイプ
if (currentThinkingBlock) {
currentThinkingBlock.signature = event.delta.signature;
}
break;
会話履歴に type:thinking
のコンテンツを載せる際には、下記のように signature
が必須なので保持しておく必要があります。
content: [
{
type: 'thinking',
thinking: "思考内容...",
signature: "Signature..."
}
]
処理全体のコードを読みたい方は下記をご覧ください。
まとめ
という感じで、Claude 3.7 Sonnet (Extended Thinking) の Stream での処理を解説しました。今回のモデルリリースで、tool_use を含めたエージェントの性能がかなり上がりそうで非常に楽しいです。
今回は、@anthropic-ai/sdk
を使用する前提で書いていますが、vercel/ai を使うのも手だと思います。
Discussion