工具调用 Hook
工具调用 Hook 能在智能体运行期间对工具执行进行细粒度控制。你可以通过这些 Hook 拦截工具调用、修改输入、转换输出、实现安全检查,以及添加全面日志或监控。
工具 Hook 会在两个关键点执行:
- 工具调用前:修改输入、校验参数或阻止执行
- 工具调用后:转换结果、净化输出或记录执行细节
Hook 类型
Section titled “Hook 类型”工具调用前 Hook
Section titled “工具调用前 Hook”在每次工具执行前运行,这类 Hook 可以:
- 检查并修改工具输入
- 根据条件阻止工具执行
- 为危险操作实现审批闸门
- 校验参数
- 记录工具调用
签名:
def before_hook(context: ToolCallHookContext) -> bool | None: # Return False to block execution # Return True or None to allow execution ...工具调用后 Hook
Section titled “工具调用后 Hook”在每次工具执行后运行,这类 Hook 可以:
- 修改或净化工具结果
- 添加元数据或格式化
- 记录执行结果
- 实现结果校验
- 转换输出格式
签名:
def after_hook(context: ToolCallHookContext) -> str | None: # Return modified result string # Return None to keep original result ...工具 Hook 上下文
Section titled “工具 Hook 上下文”ToolCallHookContext 对象提供对工具执行状态的完整访问:
class ToolCallHookContext: tool_name: str # Name of the tool being called tool_input: dict[str, Any] # Mutable tool input parameters tool: CrewStructuredTool # Tool instance reference agent: Agent | BaseAgent | None # Agent executing the tool task: Task | None # Current task crew: Crew | None # Crew instance tool_result: str | None # Agent-facing result string (after hooks only) raw_tool_result: Any | None # Raw Python result (after hooks only)对于带类型的工具输出,tool_result 是智能体看到的字符串。默认是 JSON。如果工具使用自定义格式,也可以是 Markdown 或其他字符串。需要原始 Python 值或字典时,请使用 raw_tool_result。
修改工具输入
Section titled “修改工具输入”重要: 始终原地修改工具输入:
# ✅ Correct - modify in-placedef sanitize_input(context: ToolCallHookContext) -> None: context.tool_input['query'] = context.tool_input['query'].lower()
# ❌ Wrong - replaces dict referencedef wrong_approach(context: ToolCallHookContext) -> None: context.tool_input = {'query': 'new query'}1. 全局 Hook 注册
Section titled “1. 全局 Hook 注册”注册适用于所有 crew 的所有工具调用的 Hook:
from crewai.hooks import register_before_tool_call_hook, register_after_tool_call_hook
def log_tool_call(context): print(f"Tool: {context.tool_name}") print(f"Input: {context.tool_input}") return None # Allow execution
register_before_tool_call_hook(log_tool_call)2. 基于装饰器的注册
Section titled “2. 基于装饰器的注册”使用装饰器获得更简洁的语法:
from crewai.hooks import before_tool_call, after_tool_call
@before_tool_calldef block_dangerous_tools(context): dangerous_tools = ['delete_database', 'drop_table', 'rm_rf'] if context.tool_name in dangerous_tools: print(f"⛔ Blocked dangerous tool: {context.tool_name}") return False # Block execution return None
@after_tool_calldef sanitize_results(context): if context.tool_result and "password" in context.tool_result.lower(): return context.tool_result.replace("password", "[REDACTED]") return None3. Crew 作用域 Hook
Section titled “3. Crew 作用域 Hook”为特定 crew 实例注册 Hook:
@CrewBaseclass MyProjCrew: @before_tool_call_crew def validate_tool_inputs(self, context): # Only applies to this crew if context.tool_name == "web_search": if not context.tool_input.get('query'): print("❌ Invalid search query") return False return None
@after_tool_call_crew def log_tool_results(self, context): # Crew-specific tool logging print(f"✅ {context.tool_name} completed") return None
@crew def crew(self) -> Crew: return Crew( agents=self.agents, tasks=self.tasks, process=Process.sequential, verbose=True )1. 安全护栏
Section titled “1. 安全护栏”@before_tool_calldef safety_check(context: ToolCallHookContext) -> bool | None: # Block tools that could cause harm destructive_tools = [ 'delete_file', 'drop_table', 'remove_user', 'system_shutdown' ]
if context.tool_name in destructive_tools: print(f"🛑 Blocked destructive tool: {context.tool_name}") return False
# Warn on sensitive operations sensitive_tools = ['send_email', 'post_to_social_media', 'charge_payment'] if context.tool_name in sensitive_tools: print(f"⚠️ Executing sensitive tool: {context.tool_name}")
return None2. 人工审批闸门
Section titled “2. 人工审批闸门”@before_tool_calldef require_approval_for_actions(context: ToolCallHookContext) -> bool | None: approval_required = [ 'send_email', 'make_purchase', 'delete_file', 'post_message' ]
if context.tool_name in approval_required: response = context.request_human_input( prompt=f"Approve {context.tool_name}?", default_message=f"Input: {context.tool_input}\nType 'yes' to approve:" )
if response.lower() != 'yes': print(f"❌ Tool execution denied: {context.tool_name}") return False
return None3. 输入校验与净化
Section titled “3. 输入校验与净化”@before_tool_calldef validate_and_sanitize_inputs(context: ToolCallHookContext) -> bool | None: # Validate search queries if context.tool_name == 'web_search': query = context.tool_input.get('query', '') if len(query) < 3: print("❌ Search query too short") return False
# Sanitize query context.tool_input['query'] = query.strip().lower()
# Validate file paths if context.tool_name == 'read_file': path = context.tool_input.get('path', '') if '..' in path or path.startswith('/'): print("❌ Invalid file path") return False
return None4. 结果净化
Section titled “4. 结果净化”@after_tool_calldef sanitize_sensitive_data(context: ToolCallHookContext) -> str | None: if not context.tool_result: return None
import re result = context.tool_result
# Remove API keys result = re.sub( r'(api[_-]?key|token)["\']?\s*[:=]\s*["\']?[\w-]+', r'\1: [REDACTED]', result, flags=re.IGNORECASE )
# Remove email addresses result = re.sub( r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '[EMAIL-REDACTED]', result )
# Remove credit card numbers result = re.sub( r'\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b', '[CARD-REDACTED]', result )
return result5. 工具使用分析
Section titled “5. 工具使用分析”import timefrom collections import defaultdict
tool_stats = defaultdict(lambda: {'count': 0, 'total_time': 0, 'failures': 0})
@before_tool_calldef start_timer(context: ToolCallHookContext) -> None: context.tool_input['_start_time'] = time.time() return None
@after_tool_calldef track_tool_usage(context: ToolCallHookContext) -> None: start_time = context.tool_input.get('_start_time', time.time()) duration = time.time() - start_time
tool_stats[context.tool_name]['count'] += 1 tool_stats[context.tool_name]['total_time'] += duration
if not context.tool_result or 'error' in context.tool_result.lower(): tool_stats[context.tool_name]['failures'] += 1
print(f""" 📊 Tool Stats for {context.tool_name}: - Executions: {tool_stats[context.tool_name]['count']} - Avg Time: {tool_stats[context.tool_name]['total_time'] / tool_stats[context.tool_name]['count']:.2f}s - Failures: {tool_stats[context.tool_name]['failures']} """)
return None6. 速率限制
Section titled “6. 速率限制”from collections import defaultdictfrom datetime import datetime, timedelta
tool_call_history = defaultdict(list)
@before_tool_calldef rate_limit_tools(context: ToolCallHookContext) -> bool | None: tool_name = context.tool_name now = datetime.now()
# Clean old entries (older than 1 minute) tool_call_history[tool_name] = [ call_time for call_time in tool_call_history[tool_name] if now - call_time < timedelta(minutes=1) ]
# Check rate limit (max 10 calls per minute) if len(tool_call_history[tool_name]) >= 10: print(f"🚫 Rate limit exceeded for {tool_name}") return False
# Record this call tool_call_history[tool_name].append(now) return None7. 缓存工具结果
Section titled “7. 缓存工具结果”import hashlibimport json
tool_cache = {}
def cache_key(tool_name: str, tool_input: dict) -> str: """Generate cache key from tool name and input.""" input_str = json.dumps(tool_input, sort_keys=True) return hashlib.md5(f"{tool_name}:{input_str}".encode()).hexdigest()
@before_tool_calldef check_cache(context: ToolCallHookContext) -> bool | None: key = cache_key(context.tool_name, context.tool_input) if key in tool_cache: print(f"💾 Cache hit for {context.tool_name}") # Note: Can't return cached result from before hook # Would need to implement this differently return None
@after_tool_calldef cache_result(context: ToolCallHookContext) -> None: if context.tool_result: key = cache_key(context.tool_name, context.tool_input) tool_cache[key] = context.tool_result print(f"💾 Cached result for {context.tool_name}") return None8. 调试日志
Section titled “8. 调试日志”@before_tool_calldef debug_tool_call(context: ToolCallHookContext) -> None: print(f""" 🔍 Tool Call Debug: - Tool: {context.tool_name} - Agent: {context.agent.role if context.agent else 'Unknown'} - Task: {context.task.description[:50] if context.task else 'Unknown'}... - Input: {context.tool_input} """) return None
@after_tool_calldef debug_tool_result(context: ToolCallHookContext) -> None: if context.tool_result: result_preview = context.tool_result[:200] print(f"✅ Result Preview: {result_preview}...") else: print("⚠️ No result returned") return NoneHook 管理
Section titled “Hook 管理”取消注册 Hook
Section titled “取消注册 Hook”from crewai.hooks import ( unregister_before_tool_call_hook, unregister_after_tool_call_hook)
# Unregister specific hookdef my_hook(context): ...
register_before_tool_call_hook(my_hook)# Later...success = unregister_before_tool_call_hook(my_hook)print(f"Unregistered: {success}")清除 Hook
Section titled “清除 Hook”from crewai.hooks import ( clear_before_tool_call_hooks, clear_after_tool_call_hooks, clear_all_tool_call_hooks)
# Clear specific hook typecount = clear_before_tool_call_hooks()print(f"Cleared {count} before hooks")
# Clear all tool hooksbefore_count, after_count = clear_all_tool_call_hooks()print(f"Cleared {before_count} before and {after_count} after hooks")列出已注册 Hook
Section titled “列出已注册 Hook”from crewai.hooks import ( get_before_tool_call_hooks, get_after_tool_call_hooks)
# Get current hooksbefore_hooks = get_before_tool_call_hooks()after_hooks = get_after_tool_call_hooks()
print(f"Registered: {len(before_hooks)} before, {len(after_hooks)} after")条件式 Hook 执行
Section titled “条件式 Hook 执行”@before_tool_calldef conditional_blocking(context: ToolCallHookContext) -> bool | None: # Only block for specific agents if context.agent and context.agent.role == "junior_agent": if context.tool_name in ['delete_file', 'send_email']: print(f"❌ Junior agents cannot use {context.tool_name}") return False
# Only block during specific tasks if context.task and "sensitive" in context.task.description.lower(): if context.tool_name == 'web_search': print("❌ Web search blocked for sensitive tasks") return False
return None感知上下文的输入修改
Section titled “感知上下文的输入修改”@before_tool_calldef enhance_tool_inputs(context: ToolCallHookContext) -> None: # Add context based on agent role if context.agent and context.agent.role == "researcher": if context.tool_name == 'web_search': # Add domain restrictions for researchers context.tool_input['domains'] = ['edu', 'gov', 'org']
# Add context based on task if context.task and "urgent" in context.task.description.lower(): if context.tool_name == 'send_email': context.tool_input['priority'] = 'high'
return Nonetool_call_chain = []
@before_tool_calldef track_tool_chain(context: ToolCallHookContext) -> None: tool_call_chain.append({ 'tool': context.tool_name, 'timestamp': time.time(), 'agent': context.agent.role if context.agent else 'Unknown' })
# Detect potential infinite loops recent_calls = tool_call_chain[-5:] if len(recent_calls) == 5 and all(c['tool'] == context.tool_name for c in recent_calls): print(f"⚠️ Warning: {context.tool_name} called 5 times in a row")
return None- 保持 Hook 聚焦:每个 Hook 只应有一个职责
- 避免重计算:Hook 会在每次工具调用时执行
- 优雅处理错误:使用 try-except 防止 Hook 失败
- 使用类型提示:利用
ToolCallHookContext获得更好的 IDE 支持 - 记录阻塞条件:清楚说明何时以及为什么阻止工具执行
- 独立测试 Hook:在生产使用前进行单元测试
- 在测试中清理 Hook:测试运行之间使用
clear_all_tool_call_hooks() - 原地修改:始终原地修改
context.tool_input,不要替换 - 记录重要决策:尤其是在阻止工具执行时
- 考虑性能:在可能时缓存昂贵的校验
@before_tool_calldef safe_validation(context: ToolCallHookContext) -> bool | None: try: # Your validation logic if not validate_input(context.tool_input): return False except Exception as e: print(f"⚠️ Hook error: {e}") # Decide: allow or block on error return None # Allow execution despite errorfrom crewai.hooks import ToolCallHookContext, BeforeToolCallHookType, AfterToolCallHookType
# Explicit type annotationsdef my_before_hook(context: ToolCallHookContext) -> bool | None: return None
def my_after_hook(context: ToolCallHookContext) -> str | None: return None
# Type-safe registrationregister_before_tool_call_hook(my_before_hook)register_after_tool_call_hook(my_after_hook)与现有工具集成
Section titled “与现有工具集成”包装现有校验
Section titled “包装现有校验”def existing_validator(tool_name: str, inputs: dict) -> bool: """Your existing validation function.""" # Your validation logic return True
@before_tool_calldef integrate_validator(context: ToolCallHookContext) -> bool | None: if not existing_validator(context.tool_name, context.tool_input): print(f"❌ Validation failed for {context.tool_name}") return False return None记录到外部系统
Section titled “记录到外部系统”import logging
logger = logging.getLogger(__name__)
@before_tool_calldef log_to_external_system(context: ToolCallHookContext) -> None: logger.info(f"Tool call: {context.tool_name}", extra={ 'tool_name': context.tool_name, 'tool_input': context.tool_input, 'agent': context.agent.role if context.agent else None }) return NoneHook 没有执行
Section titled “Hook 没有执行”- 确认 Hook 在 crew 执行前已注册
- 检查前一个 Hook 是否返回了
False(会阻止执行以及后续 Hook) - 确认 Hook 签名与预期类型一致
输入修改没有生效
Section titled “输入修改没有生效”- 使用原地修改:
context.tool_input['key'] = value - 不要替换字典:
context.tool_input = {}
结果修改没有生效
Section titled “结果修改没有生效”- 从 after hook 返回修改后的字符串
- 返回
None会保留原始结果 - 确认工具确实返回了结果
工具被意外阻止
Section titled “工具被意外阻止”- 检查所有 before hook 是否存在阻塞条件
- 验证 Hook 执行顺序
- 添加调试日志以找出是哪一个 Hook 在阻止
工具调用 Hook 为控制和监控 CrewAI 中的工具执行提供了强大能力。你可以用它们实现安全护栏、审批闸门、输入校验、结果净化、日志记录和分析。结合适当的错误处理和类型安全,Hook 能帮助你构建安全且适合生产环境的智能体系统,并具备完整可观测性。