读完这篇你能:把回复改成流式接收,理解流式消息里多出来的标志和「块」。
前置:纯聊天。约 25 分钟。
需求:通过 agent sdk 实现聊天消息的流逝输出
消息
流式消息的结构与上一个例子不同了,消息开始增加一些标志,而且引入了块的概念。

流式部分消息
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

完整代码
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 单独拎出来渲染。