第 5 课:多轮对话 Client
为什么需要多轮对话
query() 是一次性的,AI 不记得之前说过什么。但私人助理需要记住上下文:
你: 加个任务:写周报
AI: ✅ 已添加
你: 把它改成高优先级 ← AI 需要知道"它"是什么
AI: ✅ 已将"写周报"改为高优先级
你: 再加一个:整理文档
AI: ✅ 已添加
你: 看看现在有什么任务
AI: 📋 你有 2 个任务... ← AI 记得之前加了什么
ClaudeSDKClient 就是为这种场景设计的。
基础多轮对话
import asyncio
from claude_agent_sdk import (
ClaudeSDKClient, ClaudeAgentOptions,
AssistantMessage, TextBlock, ToolUseBlock,
create_sdk_mcp_server,
)
# 假设 tools/ 里的工具已定义
# from tools.todo import add_todo, list_todos, complete_todo, delete_todo
# from tools.notes import add_note, search_notes, list_recent_notes
SYSTEM_PROMPT = """你是私人助理 MiniClaw。
工具:
- 待办: add_todo, list_todos, complete_todo, delete_todo
- 笔记: add_note, search_notes, list_recent_notes
规则:
- 理解上下文,记住之前的对话
- 用中文,简洁友好
- 不确定就追问"""
async def main():
options = ClaudeAgentOptions(
system_prompt=SYSTEM_PROMPT,
mcp_servers={"productivity": productivity_tools},
allowed_tools=[...], # 工具列表
)
print("🤖 MiniClaw 上线!(quit 退出)\n")
async with ClaudeSDKClient(options=options) as client:
while True:
user_input = input("你: ").strip()
if user_input.lower() in ['quit', 'exit', 'q']:
print("🤖 拜拜!")
break
if not user_input:
continue
await client.query(user_input)
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(f"🤖: {block.text}")
elif isinstance(block, ToolUseBlock):
print(f" 🔧 {block.name}")
print()
asyncio.run(main)
对话示例
你: 加个任务:写周报
🔧 add_todo
🤖: ✅ 已添加任务 #1「写周报」
你: 优先级调高
🤖: 你是说把刚才的「写周报」优先级调高吗?
(AI 记得上下文,但需要确认——因为没有 edit_todo 工具)
你: 记一下:今天学了 MCP 自定义工具的用法
🔧 add_note
🤖: 📝 已保存笔记,标签: #学习 #MCP
你: 看看今天记了什么
🔧 list_recent_notes
🤖: 📓 最近 1 条笔记:
[2025-03-15 14:30] 今天学了 MCP 自定义工具的用法 #学习 #MCP
中断处理
有时候 AI 处理太久,需要打断:
import asyncio
async def main():
async with ClaudeSDKClient(options=options) as client:
# 发送一个可能很久的任务
await client.query("列出 /home 目录下所有文件的详细信息")
# 后台接收消息
async def receive():
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(f"🤖: {block.text[:100]}...")
task = asyncio.create_task(receive())
# 5 秒后中断
await asyncio.sleep(5)
print("\n⏱️ 太久了,中断!")
await client.interrupt()
await task
# 发新指令
await client.query("算了,只看桌面上的文件就行")
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(f"🤖: {block.text}")
动态调整
对话过程中可以改设置:
async with ClaudeSDKClient(options=options) as client:
# 简单问答用 Haiku(快、便宜)
await client.set_model("claude-haiku-4-5")
await client.query("今天周几?")
async for msg in client.receive_response():
display(msg)
# 需要深度分析时切 Sonnet
await client.set_model("claude-sonnet-4-5")
await client.query("帮我分析一下这周的工作效率,给出改进建议")
async for msg in client.receive_response():
display(msg)
# 需要 AI 直接操作文件时放开权限
await client.set_permission_mode("acceptEdits")
await client.query("把笔记导出到 notes.md 文件")
async for msg in client.receive_response():
display(msg)
消息类型处理
from claude_agent_sdk import (
AssistantMessage, UserMessage, SystemMessage, ResultMessage,
TextBlock, ToolUseBlock, ToolResultBlock, ThinkingBlock,
)
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
# AI 的文字回复 → 显示
print(f"🤖: {block.text}")
elif isinstance(block, ToolUseBlock):
# AI 调了工具 → 显示进度
print(f" 🔧 正在 {block.name}...")
elif isinstance(block, ThinkingBlock):
# AI 在思考 → 可以选择显示或隐藏
pass
elif isinstance(msg, UserMessage):
for block in msg.content:
if isinstance(block, ToolResultBlock):
# 工具返回了结果 → 通常不需要显示
pass
elif isinstance(msg, ResultMessage):
# 对话轮次结束
if msg.total_cost_usd:
print(f" 💰 ${msg.total_cost_usd:.4f}")
错误处理
from claude_agent_sdk import CLIConnectionError, ProcessError
async def safe_chat():
try:
async with ClaudeSDKClient(options=options) as client:
await client.query("你好")
async for msg in client.receive_response():
display(msg)
except CLIConnectionError as e:
print(f"❌ 连接失败: {e}")
print("请检查: 1) claude-code 是否安装 2) API Key 是否设置")
except ProcessError as e:
print(f"❌ 执行出错: {e}")
except asyncio.TimeoutError:
print("⏱️ 超时了,请重试")
也可以手动管理连接:
client = ClaudeSDKClient(options=options)
try:
await client.connect()
await client.query("你好")
async with asyncio.timeout(30):
async for msg in client.receive_response():
display(msg)
except asyncio.TimeoutError:
print("超时")
finally:
await client.disconnect()
实用模式:命令前缀
给助理加一些快捷命令:
async def process_input(client, user_input: str):
"""处理用户输入,支持快捷命令"""
# 快捷命令
shortcuts = {
"/todo": "列出所有待办任务",
"/done": "列出已完成的任务",
"/notes": "列出最近的笔记",
"/today": "总结今天的任务和笔记",
"/help": None, # 特殊处理
}
if user_input.startswith("/"):
cmd = user_input.split()[0].lower()
if cmd == "/help":
print("快捷命令:")
print(" /todo - 看待办")
print(" /done - 看已完成")
print(" /notes - 看笔记")
print(" /today - 今日总结")
return
if cmd in shortcuts:
user_input = shortcuts[cmd]
await client.query(user_input)
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(f"🤖: {block.text}")
elif isinstance(block, ToolUseBlock):
print(f" 🔧 {block.name}")
本课小结
ClaudeSDKClient提供多轮对话,有上下文记忆async with自动管理连接生命周期interrupt()中断长时间运行的任务set_model()/set_permission_mode()动态调整- 快捷命令让交互更高效
课后练习
- 实现
/export命令,导出所有待办和笔记为文本 - 加一个"连续对话计数器",显示当前第几轮对话
- 实现超时自动中断:15 秒没收到回复就打断