在使用 AI Agent(智能体)构建自动化流程时,最令人头疼的问题往往是: “如何防止 Agent 乱来?” 比如误删文件、执行了高危 Bash 命令,或者在模糊指令下“瞎猜”用户意图。
Anthropic 官方推出的 Claude Agent SDK 提供了一套非常成熟的机制:can_use_tool 回调与 AskUserQuestion 工具。本文将带你实战演练如何用 Python 实现这套“人在回路”(Human-in-the-Loop)的交互流程。
💡 核心概念:什么是 can_use_tool?
在 Claude Agent SDK 中,当 Agent 想要执行一个工具(Tool)时,它不会盲目执行,而是会触发一个 can_use_tool 回调。
这给了开发者三个核心权力:
- 审批制:展示 Tool 名称和参数,由用户决定
Allow(允许)还是Deny(拒绝)。 - 干预制:在允许之前,悄悄修改 Agent 传入的参数(例如修正一个错误的文件路径)。
- 交互制:通过内置的
AskUserQuestion工具,让 Claude 主动停下来向用户寻求澄清。
🛠️ 环境准备
首先,确保安装了最新的 Claude Agent SDK:
Bash
pip install claude-agent-sdk
🏗️ 实战一:实现工具调用审批 (Tool Approval)
假设我们让 Agent 创建并删除一个文件。为了安全,我们要求 Agent 在每一步操作前都必须获得用户的命令行确认。
代码实现
Python
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, query
from claude_agent_sdk.types import (
HookMatcher,
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext
)
# 1. 定义审批回调函数
async def can_use_tool(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
print(f"\n📢 [安全检查] Claude 想要调用工具: {tool_name}")
print(f"📥 输入参数: {input_data}")
# 获取用户输入
user_choice = input("是否允许此操作? (y/n): ").strip().lower()
if user_choice == 'y':
print("✅ 用户已授权")
# 返回 Allow,可以选择修改 input_data
return PermissionResultAllow(updated_input=input_data)
else:
print("❌ 用户已拒绝")
# 返回 Deny,并附带理由,Claude 会看到这个理由并尝试调整策略
return PermissionResultDeny(message="用户拒绝了该操作,请尝试其他方案。")
# 2. 必须的 Hack:定义一个 Hook 来保持流开启
async def dummy_hook(input_data, tool_use_id, context):
return {"continue_": True}
async def main():
# 模拟用户对话流
async def prompt_stream():
yield {
"type": "user",
"message": {
"role": "user",
"content": "帮我在 /tmp 目录下创建一个 test.txt,写入 'Hello Claude',然后删除它。",
},
}
# 3. 运行查询
async for message in query(
prompt=prompt_stream(),
options=ClaudeAgentOptions(
can_use_tool=can_use_tool,
# 注意:在 Python SDK 中目前需要挂载 PreToolUse hook 以支持 can_use_tool 的流式交互
hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
),
):
if hasattr(message, "result"):
print(f"\n🏁 任务结果: {message.result}")
if __name__ == "__main__":
asyncio.run(main())
🙋♂️ 实战二:处理 Claude 的反向提问 (Clarifying Questions)
有时候 Agent 不知道该怎么选(比如:你是要 React 还是 Vue 项目?)。这时它会调用内置的 AskUserQuestion 工具。
实现逻辑
当 tool_name == "AskUserQuestion" 时,我们需要解析 input_data 中的 questions 列表,渲染给用户选择,并将结果返回。
核心代码片段
Python
def parse_user_answer(user_input: str, options: list) -> str:
"""解析用户输入的数字索引或自由文本"""
try:
idx = int(user_input) - 1
if 0 <= idx < len(options):
return options[idx]["label"]
except ValueError:
pass
return user_input # 如果不是数字,直接返回原始文本
async def handle_ask_user(input_data: dict):
answers = {}
for q in input_data.get("questions", []):
print(f"\n❓ 提问: {q['question']}")
for i, opt in enumerate(q['options']):
print(f" {i+1}. {opt['label']} ({opt['description']})")
user_val = input("你的选择 (输入数字或直接输入想法): ")
# 映射回 Claude 要求的格式
answers[q['question']] = parse_user_answer(user_val, q['options'])
return PermissionResultAllow(
updated_input={
"questions": input_data.get("questions", []),
"answers": answers
}
)
# 在 can_use_tool 中路由:
# if tool_name == "AskUserQuestion":
# return await handle_ask_user(input_data)
🌟 进阶技巧:修改 Agent 的输入 (Approve with Changes)
这是 SDK 最强大的地方之一。你可以拦截 Agent 的危险指令并进行修正。
场景:Agent 想要执行 rm -rf /。
拦截逻辑:
Python
if tool_name == "Bash" and "rm -rf /" in input_data.get("command", ""):
print("⚠️ 检测到高危指令,已自动修正为删除特定临时文件。")
input_data["command"] = "rm /tmp/safe_file.txt"
return PermissionResultAllow(updated_input=input_data)
📝 总结
通过 Claude Agent SDK 的 can_use_tool 机制,我们可以轻松实现:
- 安全性:敏感操作必经人工确认。
- 互动性:让 Agent 能够像真人助手一样反向提问。
- 可控性:动态修改 Agent 的行为逻辑。
避坑指南:
- Python 特有 Hook:目前 Python SDK 在使用
can_use_tool时,务必挂载一个返回continue_的PreToolUseHook,否则流可能会提前关闭。 - AskUserQuestion 列表:如果你自定义了
tools列表,记得一定要把"AskUserQuestion"放进去,否则 Claude 会发现自己“没法说话”。
希望这份教程能帮你构建出更安全、更聪明的 AI Agent!如果你觉得有用,欢迎在掘金点赞收藏。🚀