실행 훅
실행 훅 (Execution Hooks)
실행 훅은 CrewAI 에이전트의 런타임 동작을 세밀하게 제어하게 해줘요. 크루 실행 전후로 도는 kickoff 훅과 달리, 실행 훅은 실행 중 특정 작업 — 실행 시작부터 모든 모델 콜, 도구 콜, 태스크/플로우 메서드 스텝, 최종 출력까지 — 을 가로챕니다. @on 데코레이터 하나로 프레임워크의 모든 인터셉션 지점을 다루는 단일 계약을 제공합니다.
출처: 공식문서
본문
기본
from crewai.hooks import on, HookAborted, InterceptionPoint
@on(InterceptionPoint.PRE_TOOL_CALL, tools=["delete_file"])
def guard_deletes(ctx):
raise HookAborted(reason="file deletion is not allowed", source="policy")
지점별 데코레이터(@before_llm_call, @after_tool_call 등)도 그대로 동작합니다. 같은 엔진 위의 어댑터이기 때문이에요. (이 페이지 끝의 "지점별 데코레이터(레거시)" 참고)
계약
모든 훅은 단일 타입드 컨텍스트를 받는 동기 콜러블입니다.
from crewai.hooks import on, HookAborted, InterceptionPoint
@on(InterceptionPoint.INPUT)
def add_defaults(ctx):
# 1. 관찰: 컨텍스트에서 무엇이든 읽기
# 2. 제자리 수정: ctx.payload 또는 중첩 필드를 직접 변경
ctx.payload.setdefault("locale", "en-US")
# 3. 또는 교체: 새 값을 반환해 ctx.payload 를 교체
# 4. 또는 중단: raise HookAborted(reason, source) 로 작업 중지
return None
훅이 할 수 있는 네 가지:
| 동작 | 방법 | 효과 |
|---|---|---|
| 진행 | return None(또는 무반환) |
작업이 변경 없이 계속 |
| 수정 | ctx.payload / 필드를 제자리에서 변경 |
변경이 하위에서 보임 |
| 교체 | return new_payload |
None 이 아닌 반환이 ctx.payload 를 교체 |
| 중단 | raise HookAborted(reason, source) |
작업이 중지되고 reason 이 전파 |
훅 등록
전역 훅에는 @on 을 사용합니다. agents= / tools= 필터로 훅을 특정 에이전트 역할이나 도구 이름에 한정할 수 있어요.
from crewai.hooks import on, InterceptionPoint
@on(InterceptionPoint.POST_TOOL_CALL, agents=["researcher"], tools=["web_search"])
def log_search_results(ctx):
print(f"search returned: {(ctx.tool_result or '')[:80]}")
@CrewBase 클래스 안의 메서드에 적용하면, 그 크루가 실행되는 동안만 활성화되는 크루 범위 훅이 됩니다.
from crewai import CrewBase
from crewai.hooks import on, InterceptionPoint
@CrewBase
class MyProjCrew:
@on(InterceptionPoint.PRE_MODEL_CALL)
def validate_inputs(self, ctx):
# 이 크루에만 적용
return None
인터셉션 지점 카탈로그
각 계열에는 컨텍스트 스키마, payload 의미, 예시를 다루는 상세 가이드가 있습니다.
실행 경계(Execution boundaries)
| 지점 | 시점 | ctx.payload |
|---|---|---|
EXECUTION_START |
크루나 플로우가 시작하려 할 때 | inputs dict |
INPUT |
실행을 위한 결정된(resolved) 입력 | inputs dict |
OUTPUT |
최종 결과가 준비될 때 | 출력 객체 |
EXECUTION_END |
크루나 플로우가 끝났을 때 | 출력 객체 |
모델 경계 및 도구 경계
| 지점 | 시점 | 훅이 받는 것 |
|---|---|---|
PRE_MODEL_CALL |
LLM 호출 전 | LLMCallHookContext |
POST_MODEL_CALL |
LLM 호출 후 | LLMCallHookContext(response 설정됨) |
PRE_TOOL_CALL |
도구 실행 전 | ToolCallHookContext |
POST_TOOL_CALL |
도구 실행 후 | ToolCallHookContext(결과 설정됨) |
이 네 지점에서는 훅이 풍부한 레거시 컨텍스트를 인자로 직접 받습니다(별도의 ctx.payload 없음). ctx.messages / ctx.tool_input 을 제자리에서 수정하고, post 훅에서 문자열을 반환해 응답/도구 결과를 교체하세요.
스텝 지점
| 지점 | 시점 | ctx.payload |
|---|---|---|
PRE_STEP |
태스크나 플로우 메서드 스텝 전 | 스텝 입력 |
POST_STEP |
태스크나 플로우 메서드 스텝 후 | 스텝 출력 |
PRE_STEP / POST_STEP 는 ctx.kind("task" 또는 "flow_method")와 ctx.step_name 을 지닙니다.
작업 중단
HookAborted 는 reason 과 선택적 source 를 지닙니다. source 는 생략하면 중단한 훅으로 기본 설정되는데, 텔레메트리와 실패 메시지에 유용합니다.
@on(InterceptionPoint.EXECUTION_START)
def enforce_policy(ctx):
if not ctx.payload.get("authorized"):
raise HookAborted(reason="unauthorized execution", source="access-control")
조합, 순서, fail-open
- 한 지점의 여러 훅은 등록 순서대로 실행되며, 전역 훅이 먼저, 그 다음 실행 범위 훅입니다. 같은 지점에 등록된 레거시 훅도 같은 체인에 참여합니다.
- (변경될 수 있는) payload 가 한 훅에서 다음 훅으로 흐릅니다.
HookAborted는 설계상 전파되며 체인을 중지합니다.- 훅이 발생시킨 다른 예외는 삼켜집니다(fail-open). 버그 있는 훅 하나가 실행을 죽이지 못하게요.
- 지점에 등록된 훅이 없으면 디스패치는 단일 dict 조회(no-op fast path)이므로, 안 쓰는 지점은 사실상 비용이 없습니다.
흔한 패턴
안전 가드레일
@on(InterceptionPoint.PRE_TOOL_CALL)
def block_dangerous_tools(ctx):
dangerous = {"delete_file", "drop_table", "system_shutdown"}
if ctx.tool_name in dangerous:
raise HookAborted(reason=f"{ctx.tool_name} is blocked", source="safety-policy")
@on(InterceptionPoint.PRE_MODEL_CALL)
def iteration_limit(ctx):
if ctx.iterations > 15:
raise HookAborted(reason="maximum iterations exceeded", source="loop-guard")
인간 인 더 루프 승인
@on(InterceptionPoint.PRE_TOOL_CALL, tools=["send_email", "make_payment"])
def require_approval(ctx):
response = ctx.request_human_input(
prompt=f"Approve {ctx.tool_name}?",
default_message="Type 'yes' to approve:",
)
if response.lower() != "yes":
raise HookAborted(reason="rejected by operator", source="approval-gate")
출력 정제
None 이 아닌 반환 값이 인터셉트 가능한 값을 교체하므로, 변환은 단순한 반환문이 됩니다.
import re
@on(InterceptionPoint.POST_MODEL_CALL)
def redact_keys(ctx):
return re.sub(
r'(api[_-]?key)["\']?\s*[:=]\s*["\']?[\w-]+',
r"\1: [REDACTED]",
ctx.response,
flags=re.IGNORECASE,
)
스텝 관찰
@on(InterceptionPoint.POST_STEP)
def trace_steps(ctx):
print(f"{ctx.kind} '{ctx.step_name}' finished")
텔레메트리
어떤 지점이 실제로 하나 이상의 훅에 디스패치할 때마다, CrewAI 는 이벤트 버스에 HookDispatchedEvent 를 발행합니다. 지점, 결과(proceeded / modified / aborted), 훅 수, 지속 시간, 그리고 중단 시 reason·source를 담아요. no-op fast path 는 아무것도 발행하지 않습니다.
테스트에서 훅 관리
전역 훅은 프로세스 수명 동안 유지됩니다. 테스트 사이에 초기화하세요.
import pytest
from crewai.hooks import clear_all_hooks
@pytest.fixture(autouse=True)
def reset_hooks():
clear_all_hooks()
yield
clear_all_hooks()
베스트프랙티스
- 훅을 집중적으로 — 훅마다 책임 하나를 명확히; 모든 것을 하는 큰 훅 하나보다 작은 훅 여러 개를 등록
- 훅을 빠르게 — 훅은 자기 지점의 모든 디스패치에서 실행됩니다. 무거운 계산과 무거운 의존성의 lazy import 를 피하세요
- 범위 지정 선호 — 무조건적 전역 훅 대신
agents=/tools=필터와 크루 범위 등록 사용 - 크게 중단 — 의미 있는
reason·source와 함께HookAborted를 발생시키세요. 그 컨텍스트는 오류 메시지와 텔레메트리에 표면화됩니다. 다른 예외는 삼켜진다는 점(fail-open)을 기억하세요. 실행을 멈추려고ValueError를 발생시키는 데 의존하지 마세요
지점별 데코레이터(레거시)
@on 이전에는 LLM·도구 콜을 전용 데코레이터 쌍으로 훅했습니다. 이들은 그대로 동작하며, 같은 디스패처의 어댑터라서 @on 훅과 같은 등록 순서 체인에서 조합됩니다.
from crewai.hooks import before_llm_call, after_llm_call, before_tool_call, after_tool_call
@before_llm_call
def limit_iterations(context):
if context.iterations > 10:
return False # 실행 차단
@after_tool_call
def log_tool_result(context):
print(f"Tool {context.tool_name} completed")
@on 과의 차이점:
- 네 개의 모델/도구 지점만 다룹니다 — 실행 경계나 스텝은 없음
- 차단은
return False이며, 중단 reason·source 가 붙지 않음 @on훅이 모델/도구 지점에서 받는 것과 같은 풍부한 컨텍스트(LLMCallHookContext(전체 executor 접근 포함),ToolCallHookContext)를 받습니다- 크루 범위 지정은 동일:
@CrewBase클래스 안의 메서드에 데코레이터 적용 - 같은
agents=/tools=필터 지원
이미 return False 의미론을 쓰는 기존 코드베이스나 지점별 타입 시그니처를 원할 때 선호할 수 있습니다. 상세 가이드(컨텍스트 속성, 패턴, 관리 API register_* / unregister_* / clear_*)는 LLM Call Hooks 와 Tool Call Hooks 를 참고하세요.