실행 훅

실행 훅 (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_STEPctx.kind("task" 또는 "flow_method")와 ctx.step_name 을 지닙니다.

작업 중단

HookAbortedreason 과 선택적 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 를 참고하세요.

더 알아보기