Human Feedback in Flows

Human Feedback in Flows (플로우 안의 사람 피드백)

AI가 만든 결과를 바로 내보내기 전에 사람의 확인을 거치게 하려면, Flow 실행을 멈추고 피드백을 받아 처리 흐름을 이어가는 게 필요해요. CrewAI의 @human_feedback 데코레이터는 바로 이 휴먼 인 더 루프(HITL) 워크플로를 Flow 안에 직접 넣어 줍니다. 품질 검수, 결정 게이트, 승인/거절/수정 패턴, 반복 개선 같은 시나리오에 적합해요.

출처: 공식문서

본문

@human_feedback 데코레이터는 CrewAI 1.8.0 이상이 필요합니다. 사용 전에 설치 버전을 확인하세요.

@human_feedback은 Flow 실행을 멈추고, 산출물을 사람에게 보여 리뷰하게 한 뒤, 피드백을 수집하고 (선택적으로) 피드백 결과에 따라 다른 리스너로 라우팅합니다.

flowchart LR
    A[Flow Method] --> B[Output Generated]
    B --> C[Human Reviews]
    C --> D{Feedback}
    D -->|emit specified| E[LLM Collapses to Outcome]
    D -->|no emit| F[HumanFeedbackResult]
    E --> G["@listen('approved')"]
    E --> H["@listen('rejected')"]
    F --> I[Next Listener]

빠른 시작

from crewai.flow.flow import Flow, start, listen
from crewai.flow.human_feedback import human_feedback

class SimpleReviewFlow(Flow):
    @start()
    @human_feedback(message="Please review this content:")
    def generate_content(self):
        return "This is AI-generated content that needs review."

    @listen(generate_content)
    def process_feedback(self, result):
        print(f"Content: {result.output}")
        print(f"Human said: {result.feedback}")

flow = SimpleReviewFlow()
flow.kickoff()

이 Flow는 ① generate_content를 실행하고 결과를 반환하고, ② 요청 메시지와 함께 출력을 사용자에게 보여주고, ③ 사용자가 피드백을 입력할 때까지 기다렸다가(Enter로 건너뛰기 가능), ④ HumanFeedbackResult 객체를 process_feedback에 전달합니다.

@human_feedback 데코레이터

파라미터

Parameter Type Required Description
message str Yes The message shown to the human alongside the method output
emit Sequence[str] No List of possible outcomes. Feedback is collapsed to one of these, which triggers @listen decorators
llm str | BaseLLM When emit specified LLM used to interpret feedback and map to an outcome
default_outcome str No Outcome to use if no feedback provided. Must be in emit
metadata dict No Additional data for enterprise integrations
provider HumanFeedbackProvider No Custom provider for async/non-blocking feedback. See Async Human Feedback
learn bool No Enable HITL learning: distill lessons from feedback and pre-review future output. Default False.
learn_limit int No Max past lessons to recall for pre-review. Default 5

기본 사용 (라우팅 없음)

emit을 지정하지 않으면 데코레이터는 피드백만 수집하고 다음 리스너에 HumanFeedbackResult를 전달합니다.

@start()
@human_feedback(message="What do you think of this analysis?")
def analyze_data(self):
    return "Analysis results: Revenue up 15%, costs down 8%"

@listen(analyze_data)
def handle_feedback(self, result):
    # result is a HumanFeedbackResult
    print(f"Analysis: {result.output}")
    print(f"Feedback: {result.feedback}")

emit으로 라우팅

emit을 지정하면 데코레이터가 라우터가 됩니다. 사람의 자유 형식 피드백을 LLM이 해석해 지정된 결과 중 하나로 축약합니다.

from crewai.flow.flow import Flow, start, listen, or_
from crewai.flow.human_feedback import human_feedback

class ReviewFlow(Flow):
    @start()
    def generate_content(self):
        return "Draft blog post content here..."

    @human_feedback(
        message="Do you approve this content for publication?",
        emit=["approved", "rejected", "needs_revision"],
        llm="gpt-4o-mini",
        default_outcome="needs_revision",
    )
    @listen(or_("generate_content", "needs_revision"))
    def review_content(self):
        return "Draft blog post content here..."

    @listen("approved")
    def publish(self, result):
        print(f"Publishing! User said: {result.feedback}")

    @listen("rejected")
    def discard(self, result):
        print(f"Discarding. Reason: {result.feedback}")

사람이 "more detail이 필요해"라고 하면 LLM이 이를 "needs_revision"으로 축약해 or_()를 통해 review_content를 다시 실행하는 수정 루프를 만듭니다. 결과가 "approved""rejected"가 될 때까지 계속되죠.

LLM은 사용 가능할 때 구조화 출력(함수 호출)을 사용해 응답이 지정된 결과 중 하나임을 보장합니다. 이렇게 하면 라우팅이 신뢰성 있고 예측 가능해져요.

@start() 메서드는 Flow 시작 시 한 번만 실행됩니다. 수정 루프가 필요하면 시작 메서드와 리뷰 메서드를 분리하고, 리뷰 메서드에 @listen(or_("trigger", "revision_outcome"))을 써서 셀프 루프를 활성화하세요.

HumanFeedbackResult

HumanFeedbackResult dataclass에 사람 피드백 상호작용의 모든 정보가 담깁니다.

from crewai.flow.human_feedback import HumanFeedbackResult

@dataclass
class HumanFeedbackResult:
    output: Any              # The original method output shown to the human
    feedback: str            # The raw feedback text from the human
    outcome: str | None      # The collapsed outcome (if emit was specified)
    timestamp: datetime      # When the feedback was received
    method_name: str         # Name of the decorated method
    metadata: dict           # Any metadata passed to the decorator

emit이 있는 @human_feedback 메서드에 의해 트리거된 리스너는 HumanFeedbackResult를 받습니다.

@listen("approved")
def on_approval(self, result: HumanFeedbackResult):
    print(f"Original output: {result.output}")
    print(f"User feedback: {result.feedback}")
    print(f"Outcome: {result.outcome}")  # "approved"
    print(f"Received at: {result.timestamp}")

피드백 히스토리 접근

Flow 클래스는 피드백에 접근하는 두 가지 속성을 제공합니다.

  • last_human_feedback — 가장 최근의 HumanFeedbackResult
  • human_feedback_history — Flow 중 수집된 모든 HumanFeedbackResult의 리스트

HumanFeedbackResulthuman_feedback_history에 추가되므로 여러 피드백 단계가 서로 덮어쓰지 않습니다. 이 리스트로 Flow 중 수집된 모든 피드백에 접근할 수 있어요.

결합 패턴

@human_feedback은 다른 데코레이터와 함께 쓸 수 있습니다.

# One-shot review at the start of a flow (no self-loop)
@start()
@human_feedback(message="Approve?")
def generate(self):
    ...

# Linear review on a listener (no self-loop)
@listen(some_step)
@human_feedback(message="Approve?")
def review(self):
    ...

# Self-loop: review that can loop back for revisions
@start()
def generate(self):
    ...

@listen(or_("generate", "needs_revision"))
@human_feedback(message="Approve?", emit=["approved", "needs_revision"])
def review(self):
    ...

# Chained routers
@listen("approved")
def next_step(self):
    ...

모범 사례

  1. 명확한 요청 메시지를 쓴다.
# ✅ Good - clear and actionable
@human_feedback(message="Approve this draft for publication? Reply 'needs_revision' to request edits.")

# ❌ Bad - vague
@human_feedback(message="Check this.")
  1. 의미 있는 결과를 고른다.
# ✅ Good - natural language outcomes
emit=["approved", "rejected", "needs_revision"]

# ❌ Bad - technical or unclear
emit=["pass", "fail"]
  1. 항상 기본 결과를 제공한다. 피드백이 없을 때 사용할 결과를 지정하세요 (default_outcomeemit에 있어야 합니다).
  2. 감사 추적에 피드백 히스토리를 사용한다. human_feedback_history로 리뷰 내역을 추적하세요.
  3. 라우팅 및 비라우팅 피드백을 모두 처리한다. result.feedbackresult.outcome을 함께 확인하세요.

비동기(논블로킹) 사람 피드백

기본적으로 @human_feedback은 콘솔 입력을 기다리며 실행을 블로킹합니다. 프로덕션에서는 Slack, 이메일, 웹훅, API 같은 외부 시스템과 통합되는 async/논블로킹 피드백이 필요할 수 있어요.

Provider 추상화provider 파라미터로 커스텀 피드백 수집 전략을 지정합니다.

from crewai.flow import Flow, start, human_feedback, HumanFeedbackProvider, HumanFeedbackPending, PendingFeedbackContext

class WebhookProvider(HumanFeedbackProvider):
    """Provider that pauses flow and waits for webhook callback."""

    def __init__(self, webhook_url: str):
        self.webhook_url = webhook_url

    def request_feedback(self, context: PendingFeedbackContext, flow: Flow) -> str:
        # Notify external system (e.g., send Slack message, create ticket)
        self.send_notification(context)

        # Pause execution - framework handles persistence automatically
        raise HumanFeedbackPending(
            context=context,
            callback_info={"webhook_url": f"{self.webhook_url}/{context.flow_id}"}
        )

class ReviewFlow(Flow):
    @start()
    @human_feedback(
        message="Review this content:",
        emit=["approved", "rejected"],
        llm="gpt-4o-mini",
        provider=WebhookProvider("https://myapp.com/api"),
    )
    def generate_content(self):
        return "AI-generated content..."

    @listen("approved")
    def publish(self, result):
        return "Published!"

HumanFeedbackPending이 발생하면 Flow 프레임워크가 상태를 자동 영속화합니다. 프로바이더는 외부 시스템에 알리고 예외만 발생시키면 되고, 수동 영속 호출은 필요 없어요.

일시정지된 Flow 처리 — async 프로바이더를 쓰면 kickoff()는 예외를 던지는 대신 HumanFeedbackPending 객체를 반환합니다.

flow = ReviewFlow()
result = flow.kickoff()

if isinstance(result, HumanFeedbackPending):
    # Flow is paused, state is automatically persisted
    print(f"Waiting for feedback at: {result.callback_info['webhook_url']}")
    print(f"Flow ID: {result.context.flow_id}")
else:
    # Normal completion
    print(f"Flow completed: {result}")

일시정지된 Flow 재개 — 피드백이 도착하면 Flow를 재개합니다.

# Sync handler:
def handle_feedback_webhook(flow_id: str, feedback: str):
    flow = ReviewFlow.from_pending(flow_id)
    result = flow.resume(feedback)
    return result

# Async handler (FastAPI, aiohttp, etc.):
async def handle_feedback_webhook(flow_id: str, feedback: str):
    flow = ReviewFlow.from_pending(flow_id)
    result = await flow.resume_async(feedback)
    return result

주요 타입:

Type Description
HumanFeedbackProvider Protocol for custom feedback providers
PendingFeedbackContext Contains all info needed to resume a paused flow
HumanFeedbackPending Returned by kickoff() when flow is paused for feedback
ConsoleProvider Default blocking console input provider

PendingFeedbackContext에는 재개에 필요한 모든 것이 들어 있습니다: flow_id, flow_class, method_name, method_output, message, emit, default_outcome, metadata, llm, requested_at.

더 알아보기