
从 query 与 ClaudeSDKClient 的选型差异出发,拆解连续会话、消息终态、中断排空、动态权限与长上下文控制,帮助开发者把 Agent 接进 Chat、REPL 和可人工接管的应用。
query() 写 Agent,单次任务很顺手:传入 prompt,遍历消息,拿到结果。resume 和全局变量,代码会越来越像一个手写的会话管理器。Python Agent SDK 已经给了一个更直接的入口:ClaudeSDKClient。query() 和 ClaudeSDKClient 不是同一个抽象| 维度 | query() | ClaudeSDKClient |
|---|---|---|
| 默认会话 | 每次调用新建 | 多次 client.query() 复用同一会话 |
| 连接管理 | SDK 自动管理 | 由你控制 connect() / disconnect(),也可以交给异步上下文管理器 |
| 对话形态 | 一次任务,一次结果 | 连续提问,下一次能接上前一轮上下文 |
| 中途打断 | 不支持 | 支持 interrupt() |
| 适合场景 | CI 任务、批处理、独立脚本 | Chat、REPL、需要人工接管的 Agent |
client.query() 发请求,client.receive_response() 消费当前请求的消息流。import asyncio
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
ClaudeSDKClient,
ResultMessage,
TextBlock,
)
async def print_response(message) -> None:
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
elif isinstance(message, ResultMessage):
if message.subtype == "success":
print(f"[done] {message.result}")
else:
print(f"[stopped] {message.subtype}")
async def main() -> None:
options = ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep"],
max_turns=8,
)
async with ClaudeSDKClient(options=options) as client:
await client.query("分析 auth 模块的登录流程")
async for message in client.receive_response():
await print_response(message)
await client.query("刚才提到的 token 在哪些文件里被创建?")
async for message in client.receive_response():
await print_response(message)
await client.query("只读审查完成后,列出三个最值得先修的风险")
async for message in client.receive_response():
await print_response(message)
asyncio.run(main())ClaudeSDKClient 会在内部维护 session ID,所以第二个 prompt 不需要手动传 resume。async with 则负责连接建立和释放;如果你需要自己管理生命周期,也可以显式调用 connect() 与 disconnect()。2receive_response() 的语义:它会持续产生消息,直到收到当前任务的 ResultMessage。中间可能有 AssistantMessage,里面既可能是文本,也可能是工具调用。ResultMessage 才是这一次 Agent loop 的终态,应该从它的 subtype 判断任务是成功、达到上限,还是执行中断。3ClaudeSDKClient 并没有把 Agent 变成普通聊天 API。每次 client.query() 仍然会启动一轮完整的 Agent loop:Claude 评估 prompt,调用工具,接收工具结果,再决定下一步,直到产生没有工具调用的最终回复。receive_response() 里可能出现很多条消息,而不是一条模型回复。一个「修复测试」任务可能先调用 Bash,再读取文件,接着编辑代码,最后再次运行测试。SDK 将这些轮次都放在当前会话里,客户端代码只负责消费事件和决定何时发出下一条用户输入。3client.query() 和 client.receive_response() 不应该混写成一个黑盒函数:前者是输入边界,后者是输出边界。做 Web UI 时,你可以在 AssistantMessage 到达时更新进度,在 ResultMessage 到达时把输入框重新打开;做 REPL 时,则可以在结果终态后等待下一条命令。interrupt() 最容易被误用的地方,是把它当成「立即清空当前任务」。官方实现并不是这样。subtype="error_during_execution" 的 ResultMessage。因此,正确顺序是:interrupt()。receive_response(),直到拿到被中断任务的终态。import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, ResultMessage
async def interruptible_task() -> None:
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
permission_mode="acceptEdits",
)
async with ClaudeSDKClient(options=options) as client:
await client.query("慢慢执行一个长任务,逐步检查当前目录中的文件")
await asyncio.sleep(2)
await client.interrupt()
interrupted_result = None
async for message in client.receive_response():
if isinstance(message, ResultMessage):
interrupted_result = message
print(f"上一任务结束状态:{interrupted_result.subtype}")
await client.query("停止刚才的计划,只输出当前已经确认的文件列表")
async for message in client.receive_response():
if isinstance(message, ResultMessage):
print(f"新任务结束状态:{message.subtype}")
asyncio.run(interruptible_task())receive_response(),你读到的可能还是上一任务的消息,而不是新任务的结果。这个坑在 UI 里尤其隐蔽,因为界面会把旧任务的尾部消息误画到新任务下面。1break 结束消息流break 提前退出,否则可能触发 asyncio 清理问题。Agent Loop 文档也说明,ResultMessage 之后仍可能有少量尾部系统事件,正确做法是让迭代自然结束。result = None
async for message in client.receive_response():
if isinstance(message, ResultMessage):
result = message
# 迭代器自然结束后,再读取 result
if result is not None and result.subtype == "success":
print(result.result)ClaudeSDKClient 的价值不止是省掉一个 resume 参数。Python API 还提供了一组当前会话级别的控制方法:set_permission_mode(mode):改变当前会话的权限模式。set_model(model):切换当前会话使用的模型,传入 None 可恢复默认模型。get_mcp_status():读取 MCP 服务状态。reconnect_mcp_server(name):重新连接失效的 MCP 服务。toggle_mcp_server(name, enabled):在会话中启用或停用某个 MCP 服务。rewind_files(user_message_id):在启用 enable_file_checkpointing=True 后,把文件恢复到指定用户消息对应的状态。set_model()。这些方法的定义和限制见 Python SDK 参考。1ClaudeSDKClient 不是「无限记忆」开关。生产应用至少要给 Agent loop 配置 max_turns 或 max_budget_usd,并在 ResultMessage 里记录 subtype、num_turns、成本和 session_id。达到上限时,结果不会有可直接使用的成功文本,应该把它当作可恢复状态处理。3session_id 并使用 resume;多用户服务也不能把一个 client 实例给所有用户共享。Sessions 页面把这几种情况分得很清楚:当前进程多轮对话用 ClaudeSDKClient,跨进程或指定历史用 resume,探索另一条路径用 fork。2query()。ClaudeSDKClient。ClaudeSDKClient。session_id,再设计 resume。fork,不要把文件复制一份就当成会话分支。await client.query(),而是:发出一轮输入,完整消费到 ResultMessage,确认状态,再决定下一轮动作。docs.anthropic.com
docs.anthropic.com
docs.anthropic.com
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.