第 4 章:多轮对话与流式交互
前面几章我们一直在用 query() 函数和 Claude 对话。它简单、好用,但有一个根本性的限制:
每次调用都是独立的,Claude 不记得你之前说了什么。
这一章,我们要解锁一个新能力:让 Claude 在一段持续的对话中记住上下文。
1. query() 的局限:健忘症患者
先来看一个问题。假设你想和 Claude 聊一个话题,先问第一个问题,再基于回答追问:
from claude_agent_sdk import AssistantMessage, TextBlock, query
# 第一次问
async for msg in query(prompt="我叫小明,请记住我的名字"):
...
# 第二次问
async for msg in query(prompt="我叫什么名字?"):
...
# Claude: "抱歉,我不知道你叫什么名字。"
为什么?因为 query() 的每次调用都是一个全新的进程。就好比你给一个陌生人发短信——
你发完一条,对方回了,然后你换了个新号码又发了一条。对方当然不知道你是谁。
query() 像发短信 —— 发完就完了,对方不记得你之前说了什么。
ClaudeSDKClient 像打电话 —— 你拨通了,一直保持通话,对方始终记得整个对话的内容。
2. ClaudeSDKClient 入门
最简用法
import asyncio
from claude_agent_sdk import (
AssistantMessage, ClaudeSDKClient, ResultMessage, TextBlock
)
async def main():
# async with 自动处理连接和断开
async with ClaudeSDKClient() as client:
# 发送消息
await client.query("你好,我叫小明!")
# 接收回复
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(block.text)
asyncio.run(main())
三个关键步骤:
| 步骤 | 代码 | 说明 |
|---|---|---|
| 1. 建立连接 | async with ClaudeSDKClient() as client |
启动一个持续的会话 |
| 2. 发送消息 | await client.query("...") |
把消息发给 Claude |
| 3. 接收回复 | async for msg in client.receive_response() |
逐条接收回复,直到收到 ResultMessage |
async with 帮你自动处理了连接(connect())和断开(disconnect()),
就像 Python 的 with open() 自动帮你关文件一样。你不需要操心资源清理的问题。
和 query() 的对比
| 特性 | query() |
ClaudeSDKClient |
|---|---|---|
| 上下文记忆 | 无 | 有 |
| 多轮对话 | 不支持 | 支持 |
| 中断 | 不支持 | 支持 |
| 动态配置 | 不支持 | 支持(切换模型、权限等) |
| 使用难度 | 简单 | 稍复杂 |
| 适合场景 | 一问一答 | 持续交互 |
3. 多轮对话
ClaudeSDKClient 真正的威力在于:你可以连续发送多个 query(),Claude 会记住之前的所有对话。
import asyncio
from claude_agent_sdk import (
AssistantMessage, ClaudeSDKClient, ResultMessage, TextBlock
)
def print_response(msg):
"""打印 Claude 回复中的文字内容。"""
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
async def main():
async with ClaudeSDKClient() as client:
# ---- 第一轮:告诉 Claude 你的名字 ----
await client.query("我叫小明,我是一个 Python 开发者。请记住这些信息。")
async for msg in client.receive_response():
print_response(msg)
print("---")
# ---- 第二轮:追问,Claude 应该记得 ----
await client.query("我叫什么名字?我是做什么的?")
async for msg in client.receive_response():
print_response(msg)
print("---")
# ---- 第三轮:基于前面的对话继续 ----
await client.query("给我推荐一个适合我的 Python 项目练手。")
async for msg in client.receive_response():
print_response(msg)
asyncio.run(main())
输出大概是这样的:
Claude: 好的,我记住了!你叫小明,是一个 Python 开发者。
---
Claude: 你叫小明,你是一个 Python 开发者。
---
Claude: 基于你是 Python 开发者的背景,我推荐你可以试试...
注意第三轮:Claude 不仅记得你的名字,还记得你是 Python 开发者,所以它的推荐是有针对性的。 这就是多轮对话的价值——对话有了连续性,Claude 能基于完整的上下文做出更好的回答。
session_id:给对话起个名字
query() 方法有一个可选参数 session_id,默认值是 "default"。
同一个 session_id 下的消息共享上下文:
# 这两条消息在同一个 session 里,Claude 能记住上下文
await client.query("我叫小明", session_id="chat-1")
await client.query("我叫什么?", session_id="chat-1")
# 这条消息在另一个 session 里,Claude 不知道你是谁
await client.query("我叫什么?", session_id="chat-2")
在大多数场景下,你不需要指定 session_id,用默认值就好。
它主要用于需要管理多个独立对话的高级场景(比如同时服务多个用户)。
完整代码见
examples/multi_session.py
4. receive_response() vs receive_messages()
ClaudeSDKClient 有两种接收消息的方式,别搞混了:
receive_response() — 推荐,最常用
async for msg in client.receive_response():
# 逐条处理消息
# 收到 ResultMessage 后自动停止
pass
receive_response() 会一直接收消息,直到收到一个 ResultMessage(表示本轮回复结束),
然后自动停止迭代。这是你 90% 的场景应该用的。
每次你调用 query() 发一条消息,就用 receive_response() 接收对应的回复。
一发一收,节奏清晰。
receive_messages() — 持续监听,不自动停止
async for msg in client.receive_messages():
# 持续接收所有消息
# 不会自动停止!需要你自己 break
if should_stop(msg):
break
receive_messages() 会无限接收消息,永远不会自动停止。
就像开了一个"消息监控器",所有消息都会经过这里。
什么时候用 receive_messages()?
- 你想在后台持续监听所有消息
- 你需要自定义停止条件(比如收到特定内容就停)
- 你在做更底层的消息处理
日常开发:用 receive_response()。 简单、安全、不会忘记停止。
5. 流式输出 — "打字机"效果
到目前为止,我们处理的都是"完整消息"——Claude 把一整段话想完了,打包发给你。 但实际上 Claude 是一个字一个字生成的,你可以利用这个特性做出"打字机"效果, 让用户感觉 Claude 在实时打字。
要实现这个效果,你需要开启 部分消息流(partial messages):
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
from claude_agent_sdk.types import StreamEvent
# 关键:设置 include_partial_messages=True
options = ClaudeAgentOptions(
include_partial_messages=True,
)
开启后,除了完整的 AssistantMessage,你还会收到 StreamEvent 类型的消息。
每个 StreamEvent 携带一小块增量数据,对应 Anthropic API 的原始流事件。
从 StreamEvent 中提取文字增量
StreamEvent 的核心结构是 event 字段,它是一个字典,包含 Anthropic API 的原始流事件。
最常用的事件类型是 content_block_delta,里面有文字增量:
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, ResultMessage
from claude_agent_sdk.types import StreamEvent
async def main():
options = ClaudeAgentOptions(
include_partial_messages=True,
)
async with ClaudeSDKClient(options) as client:
await client.query("用三句话介绍 Python 的历史")
async for msg in client.receive_response():
if isinstance(msg, StreamEvent):
event = msg.event
# 提取文字增量
if event.get("type") == "content_block_delta":
delta = event.get("delta", {})
if delta.get("type") == "text_delta":
# 逐字打印,不换行
print(delta["text"], end="", flush=True)
elif isinstance(msg, ResultMessage):
print() # 最后换行
print(f"\n(耗时 {msg.duration_ms}ms)")
asyncio.run(main())
运行效果:文字会像打字机一样,一个词一个词地蹦出来,而不是等 Claude 全部想完才一次性显示。
StreamEvent 中的常见事件类型
| event.type | 说明 | 包含的数据 |
|---|---|---|
message_start |
消息开始 | 模型信息、usage 等 |
content_block_start |
内容块开始 | 内容块类型(text/tool_use 等) |
content_block_delta |
内容块增量 | delta 中有 text_delta 或 input_json_delta |
content_block_stop |
内容块结束 | 无额外数据 |
message_delta |
消息级别更新 | stop_reason、usage 更新 |
message_stop |
消息结束 | 无额外数据 |
对于打字机效果,你只需要关心 content_block_delta + text_delta。其他事件类型在做
更高级的流处理时才会用到。
完整实现见
examples/typewriter.py
6. 动态配置:运行时切换模型和权限
ClaudeSDKClient 有一个 query() 做不到的能力:在对话过程中动态调整配置。
set_model() — 运行时切换模型
async with ClaudeSDKClient() as client:
# 先用默认模型(通常是 Sonnet)做快速分析
await client.query("分析这段代码有什么问题:def foo(): return 1/0")
async for msg in client.receive_response():
print_response(msg)
# 切换到更强的模型做深度优化
await client.set_model("claude-opus-4-1-20250805")
await client.query("请给出一个完整的、带错误处理的改进方案")
async for msg in client.receive_response():
print_response(msg)
场景:先用便宜模型做粗筛,再用贵的模型做精细活。
比如你在做代码审查:先让 Sonnet 快速扫一遍找出潜在问题,然后对有疑问的地方切换到 Opus 做深入分析。这样既省钱又不牺牲质量。
传 None 可以切回默认模型:
await client.set_model(None) # 切回默认模型
set_permission_mode() — 运行时切换权限
async with ClaudeSDKClient() as client:
# 先用安全模式做分析(Claude 不会修改任何文件)
await client.query("帮我看看这个项目的结构")
async for msg in client.receive_response():
print_response(msg)
# 分析完了,切换到 acceptEdits 模式,允许 Claude 直接修改文件
await client.set_permission_mode("acceptEdits")
await client.query("把 README 里的拼写错误修复一下")
async for msg in client.receive_response():
print_response(msg)
四种权限模式:
| 模式 | 说明 |
|---|---|
"default" |
默认模式,遇到危险操作时会暂停等待确认 |
"acceptEdits" |
自动接受文件编辑,但其他危险操作仍需确认 |
"plan" |
规划模式,Claude 只分析不执行 |
"bypassPermissions" |
跳过所有权限检查(谨慎使用!) |
安全建议: 先在限制模式下让 Claude 分析,确认方案没问题后再放开权限让它执行。
不要一上来就给 bypassPermissions。
7. 中断:按下急刹车
如果 Claude 正在做一个很长的任务,你想让它停下来,可以使用 interrupt():
import asyncio
from claude_agent_sdk import (
AssistantMessage, ClaudeSDKClient, ResultMessage, TextBlock
)
async def main():
async with ClaudeSDKClient() as client:
# 给一个会产生很长回复的任务
await client.query("列出 1 到 1000 的所有质数,每个都要解释为什么是质数")
count = 0
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(block.text[:100]) # 只打印前 100 个字符
count += 1
if count >= 3:
print("\n--- 够了,打断它!---")
await client.interrupt()
elif isinstance(msg, ResultMessage):
print(f"\n任务结束,共 {msg.num_turns} 轮")
asyncio.run(main())
注意:interrupt() 必须在你同时正在消费消息(receive_response() 或
receive_messages())的时候调用才有效。 因为中断信号需要通过消息通道来处理。
在上面的例子里,我们在 async for 循环内部调用 interrupt(),这是正确的做法。
如果你在循环外面调用,中断信号可能无法被正确处理。
更实际的中断场景:超时
import asyncio
from claude_agent_sdk import ClaudeSDKClient, ResultMessage
async def query_with_timeout(client, prompt, timeout_seconds=30):
"""带超时的查询。"""
await client.query(prompt)
async def _watchdog():
"""超时后自动中断。"""
await asyncio.sleep(timeout_seconds)
await client.interrupt()
# 启动超时看门狗
watchdog_task = asyncio.create_task(_watchdog())
try:
async for msg in client.receive_response():
# 正常处理消息...
yield msg
finally:
watchdog_task.cancel()
8. 手动连接管理
async with 很方便,但有时你需要更细粒度的控制——比如在一个长时间运行的程序里,
连接的建立和断开发生在不同的函数中。
这时候你可以手动调用 connect() 和 disconnect():
import asyncio
from claude_agent_sdk import (
AssistantMessage, ClaudeSDKClient, ResultMessage, TextBlock
)
async def main():
client = ClaudeSDKClient()
try:
# 手动连接
await client.connect()
# 发送和接收
await client.query("你好!")
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(block.text)
finally:
# 无论是否出错,确保断开连接
await client.disconnect()
asyncio.run(main())
关键:disconnect() 必须放在 finally 里。 否则如果中间出了异常,连接就泄漏了。
这就是为什么大多数时候推荐用 async with——它帮你保证了这一点。
connect() 的高级用法
connect() 可以接受一个初始 prompt,在连接的同时就开始对话:
# 方式一:先连接,再发消息(推荐)
await client.connect()
await client.query("你好")
# 方式二:连接时就发消息
await client.connect(prompt="你好")
两者效果一样。方式一更灵活,方式二更简洁。
不传参数调用 connect() 时,SDK 会以"空流"模式连接——建立好通道,
但不发送任何消息,等你后续调用 query() 再开始对话。这也是 async with
内部的默认行为。
9. 完整示例:聊天 REPL
把这一章学到的东西组合起来,做一个简单的聊天程序—— 你在终端输入消息,Claude 实时回复,就像在用 ChatGPT 一样:
import asyncio
from claude_agent_sdk import (
AssistantMessage, ClaudeSDKClient, ResultMessage, TextBlock
)
async def main():
print("MiniChat - 和 Claude 聊天(输入 /quit 退出)\n")
async with ClaudeSDKClient() as client:
while True:
user_input = input("你: ").strip()
if user_input in ("/quit", "/exit", ""):
print("再见!")
break
await client.query(user_input)
print("Claude: ", end="", flush=True)
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(block.text, end="", flush=True)
elif isinstance(msg, ResultMessage):
print() # 换行
print()
asyncio.run(main())
试试运行它,体验多轮对话的效果:
MiniChat - 和 Claude 聊天(输入 /quit 退出)
你: 我叫小明,记住我的名字
Claude: 好的,小明!我记住了。有什么我能帮你的吗?
你: 我叫什么?
Claude: 你叫小明呀!
你: /quit
再见!
完整代码见
examples/chat_loop.py
10. 小结
这一章我们从 query() 的"一次性对话"升级到了 ClaudeSDKClient 的"持续会话"。
核心知识点:
ClaudeSDKClient保持对话上下文 — 多次query()之间,Claude 记得之前说了什么receive_response()是首选 — 接收一轮完整回复后自动停止,安全省心receive_messages()用于持续监听 — 不自动停止,需要你自己控制退出- 流式输出用
StreamEvent— 开启include_partial_messages=True,逐字接收 - 动态配置 —
set_model()和set_permission_mode()随时调整 interrupt()打断任务 — 必须在消费消息的同时调用- 手动管理用
try/finally— 不用async with时确保disconnect()
下一章,我们将学习如何给 Claude 添加自定义工具——让它不仅能说,还能做。
本章文件清单
04-多轮对话与流式交互/
README.md # 你正在读的这个文件
examples/
chat_loop.py # 聊天 REPL:多轮对话循环
multi_session.py # session_id 用法与多轮上下文
typewriter.py # 打字机效果:流式输出实现