개요
대화형 앱은 각 사용자 입력을 동일한 세션 id로 새 flow 실행으로 처리합니다. CrewAI는 메시지 기록, 선택적 의도 라우팅, 지연 트레이싱, 구조화된 턴 스트리밍, 로컬flow.chat() REPL을 위한 헬퍼를 제공합니다.
턴 API
REST, WebSocket, 테스트, 커스텀 UI에서 오는 모든 사용자 메시지에는 **flow.handle_turn(message, session_id=...)**를 사용하세요. 대화형 Flow를 로컬 터미널 채팅 루프로 실행하고 싶을 때는 **flow.chat()**을 사용하세요.
Flow.kickoff()는 user_message= 또는 session_id= 키워드 인자를 받지 않습니다. 대화형 flow에서는 handle_turn()이 보류 중인 메시지를 저장하고 턴별 실행 상태를 초기화한 뒤 내부적으로 kickoff(inputs={"id": session_id})를 호출합니다.
대화형 모드가 활성화되지 않으면
handle_turn(), stream_turn(), chat()은 ValueError를 발생시킵니다. @ConversationConfig(...)를 적용하면 자동으로 활성화되며, 그렇지 않으면 conversational = True로 설정하세요.
빠른 시작
턴 스트리밍
UI나 런타임에서 한 채팅 턴의 구조화된 이벤트가 필요하면stream_turn()을 사용하세요. Flow 라우팅, LLM chunk, tool 활동, 대화 메시지를 순서가 보장된 frame으로 제공하는 stream session을 반환합니다.
턴 생명주기
각handle_turn은 다음 파이프라인을 실행합니다:
- 턴 설정 — 보류 중인 사용자 메시지를 저장하고 세션 id를 결정하며 턴별 실행 추적을 초기화한 뒤
kickoff(inputs={"id": session_id})를 호출. - 상태 복원 —
inputs["id"]가 있고@persist가 설정되면 최신 스냅샷 로드. FlowStarted— 지연 세션의 첫 턴에서만 발생.- 보류 중인 턴 수화 — 사용자 메시지를
state.messages에 추가하고current_user_message/last_user_message를 설정하며,intents/default_intents+intent_llm설정 시 선택적으로 분류. - 그래프 실행 — 사용자 정의
@start메서드(있는 경우) →route_conversation(내장 start/router) → 선택된@listen핸들러.route_conversation은 재정의 가능한conversation_start()헬퍼도 호출합니다. - 실행 종료 — 지연 활성화 시 턴별
flow_finished및 trace 종료 건너뜀; 중첩Agent.kickoff()/ crew도 부모 batch를 닫지 않음.
append_assistant_message(reply)**를 호출하세요. public 문자열 반환값도 assistant로 기록되며 @persist 스냅샷에 포함되므로, 새 Flow 인스턴스에서도 복원됩니다. 사용자 입력은 handle_turn이 이미 저장합니다 — 핸들러에서 다시 추가하지 마세요.
설정 개요
Flow 서브클래스에 ConversationConfig를 데코레이터로 적용하면 채팅 기본값이 부착되고 대화형 모드도 활성화됩니다. 아래의 전체 필드 레퍼런스를 참고하세요. 턴마다 handle_turn(..., intents=..., intent_llm=...)로 사전 분류 설정을 재정의할 수 있습니다.
하위 수준 ChatState 헬퍼
ChatState, 레거시 ConversationalConfig, crewai.flow.conversation 헬퍼는 고급 오케스트레이션, 테스트, 커스텀 래퍼에서 계속 import할 수 있습니다. 이들은 ConversationState / ConversationConfig API와 별개이며 Flow.kickoff()에 user_message= 또는 session_id= 키워드 인자를 추가하지 않습니다.
ConversationalInputs는 kickoff(inputs={...})용 TypedDict: id, user_message, last_intent.
ConversationState는 messages를 ConversationMessage 객체로 저장하며 current_user_message, ended, events, agent_threads도 제공합니다. 정식 기록을 LLM에 전달할 때는 conversation_messages를 사용하세요.
Flow 대화 API
handle_turn 파라미터
kickoff 파라미터
Flow.kickoff()는 inputs, input_files, from_checkpoint, restore_from_state_id를 받습니다. 원시 flow 실행이 필요하면 inputs={"id": session_id}를 전달할 수 있지만, 채팅 메시지를 나타내는 호출에는 handle_turn()을 사용하세요.
인스턴스 속성
메서드 및 프로퍼티
모듈 헬퍼 (crewai.flow.conversation)
테스트 또는 커스텀 오케스트레이션을 위해 crewai.flow.conversation에서 import할 수 있습니다. 이 헬퍼들은 레거시 ConversationalConfig 형태를 사용합니다. 또한 prepare_conversational_turn()은 last_intent를 지우지만, handle_turn()은 router 컨텍스트로 보존합니다.
의도 라우팅 패턴
A. ConversationConfig로 사전 분류 (가장 단순)
default_intents와 intent_llm을 설정하세요. 각 handle_turn()이 현재 메시지를 사전 분류합니다. 커스텀 route_turn()이 반환한 비어 있지 않은 결과가 우선하며, 그렇지 않으면 route_conversation이 현재 턴의 분류된 intent를 사용합니다.
B. route_turn 내부에서 분류 (풍부한 프롬프트)
default_intents=None으로 설정하면 handle_turn()은 사용자 메시지만 추가합니다. route_turn()에서 커스텀 프롬프트나 설명과 함께 classify_intent를 호출하세요:
@listen("RESEARCH") 등에서 Agent.kickoff()와 tool 사용 — 단순 LLM.call() 대신.
flow가 끝났지만 사용자는 계속 대화할 때
각handle_turn()은 하나의 그래프 실행을 완료하며, 같은 session_id로 다음 handle_turn()을 호출해 대화를 이어갑니다. 기본 지연 trace 수명 주기에서는 해당 실행이 conversation_turn_completed를 발생시키고, finalize_session_traces()가 세션을 닫을 때 FlowFinished가 한 번 발생합니다. @persist는 messages, 플래그, 컨텍스트를 복원합니다.
Persist 패턴: 전체 Flow 클래스보다 단일 종료 스텝(예: finalize)에 @persist를 두는 것이 좋습니다. 클래스 수준 persist는 매 메서드 후 저장하며, load_state는 최신 행을 사용해 같은 턴의 핸들러 업데이트를 놓칠 수 있습니다.
후속 채팅 줄에 @human_feedback를 쓰지 마세요. 특정 스텝 출력을 사람이 승인해야 할 때만 사용하세요.
대화형 Flow
Flow 서브클래스에 conversational = True를 지정하거나 @ConversationConfig(...)를 적용하면 대화형 채팅 그래프가 활성화됩니다. 베이스 Flow는 내장 start/router인 route_conversation과 converse_turn, end_conversation 리스너를 제공합니다. 사용 중단된 answer_from_history_turn 리스너는 호환성을 위해 계속 제공됩니다. 또한 state.messages를 관리하고 router LLM을 구동할 수 있으며 턴 간 trace batch를 열린 상태로 유지합니다. 여러분은 커스텀 라우트를 작성하고 나머지는 프레임워크에 맡기면 됩니다.
LLM 기반 라우터와 라우트별 핸들러로 멀티턴 챗을 만들고 싶지만 라이프사이클을 직접 배선하고 싶지 않을 때 사용하세요. 완전한 제어가 필요하면 위의 Flow[ChatState]로 내려가세요.
빠른 예제
chat()을 사용하세요:
chat()은 handle_turn()을 REPL로 감싸고, exit / quit에서 종료하며, 기본적으로 빈 줄을 건너뛰고, 세션이 끝날 때 finalize_session_traces()를 호출합니다.
ConversationConfig
클래스 단위의 챗 기본값을 부착하는 클래스 데코레이터입니다.
커스텀 라우트가 없으면 턴은
converse로 이어집니다. 커스텀 라우트와 대화/router LLM이 있으면 프레임워크가 기본 RouterConfig를 합성합니다. prompt, 라우트 목록, 설명, fallback 동작을 바꿔야 할 때만 명시적으로 제공하세요. default_intents를 설정하면 레거시 사전 분류 경로를 사용합니다.
대화 LLM을 설정하지 않으면 내장 converse_turn은 답변을 생성하는 대신 설정 안내 placeholder를 반환합니다.
RouterConfig와 자동 생성되는 라우트 카탈로그
RouterConfig.route_descriptions[label]— 명시적 오버라이드.Flow.builtin_route_descriptions[label]—converse,end, 사용 중단된answer_from_history호환 라우트용 프레임워크 기본 텍스트 (router LLM용으로 다듬어진 문구).- 메서드에 선언된
description— 선언적 flow와 DSL projection에서 사용. @listen(label)핸들러 docstring의 첫 번째 비어 있지 않은 줄.- 빈 문자열 — 설명 없이 라우트만 표시.
@listen("X") + 한 줄짜리 docstring입니다:
핸들러 이름 짓기
@listen("…")의 문자열은 Python 메서드 이름이 아니라 router 라우트 레이블(이벤트 이름)입니다. 라우트 레이블과 메서드 완료 이벤트는 하나의 트리거 namespace를 공유하므로, 핸들러 이름을 라우트와 같게 지정하면 핸들러가 자기 자신을 반복해서 다시 실행합니다.
서로 다른 메서드 이름을 사용하세요. 문서 예제에서는 handle_* 접두사를 사용합니다:
RouterConfig.prompt는 도메인 프레이밍 (어시스턴트 페르소나, 비즈니스 규칙, 톤)을 위한 자리입니다. 라우트 카탈로그는 자동 생성되니 prompt 안에 라우트 목록을 넣지 마세요. 핸들러를 추가하는 순간 동기화가 깨집니다.
빌트인 라우트
서브클래스에 같은 이름의 핸들러를 정의하면 어떤 것이든 오버라이드할 수 있습니다.
handle_turn() 시맨틱
flow.handle_turn(message)는 한 턴을 실행합니다:
- 그래프가 다시 실행되도록 턴 단위 실행 추적(
_completed_methods,_method_outputs)을 초기화합니다 — 이게 없으면 동일 인스턴스에서 반복kickoff호출 시Flow.kickoff_async가inputs={"id": ...}를 체크포인트 복원으로 간주해 2번째 턴부터 단락 회로가 발생합니다. - 사용자 메시지를
state.messages에 추가하고current_user_message/last_user_message를 설정합니다.last_intent는 이전 턴 값이 유지되어 router LLM이 신호로 활용할 수 있습니다. - 사용자 정의
@start메서드(있는 경우)를 실행한 다음 내장 start/router인route_conversation을 거쳐 선택된@listen핸들러를 실행합니다.route_conversation은 재정의 가능한conversation_start()헬퍼를 호출합니다. - router는 결정을
state.last_intent에 저장합니다 (다음 턴의 router 컨텍스트에서 보입니다). - 핸들러가 문자열을 반환했지만
append_assistant_message를 직접 호출하지 않았다면,handle_turn이 대신 추가한 뒤 갱신된state.messages를 persist합니다.@persist복원 시 assistant 턴이 포함됩니다.
handle_turn()을 호출하세요. kickoff(inputs={"id": ...})를 직접 호출하면 대화형 턴 래퍼 없이 flow 그래프가 실행됩니다.
로컬 REPL용 chat()
flow.chat()은 handle_turn() 위에 얹은 바로 쓸 수 있는 터미널 래퍼입니다:
- 사용자 메시지를 입력받습니다.
exit/quit,EOFError,KeyboardInterrupt에서 멈춥니다.handle_turn(message, session_id=...)를 호출합니다.- 어시스턴트 결과를 출력합니다.
finally블록에서 지연된 세션 trace를 finalize합니다.
chat(defer_trace_finalization=True)는 REPL 동안 인스턴스의 지연 플래그를 임시로 활성화하고 종료할 때 이전 값으로 복원합니다.
주입 가능한 I/O로 터미널 동작을 커스터마이즈할 수 있습니다:
handle_turn()을 직접 사용하세요.
커스텀 router 동작
매 라우팅 결정마다 사이드 이펙트(이벤트 버스 셋업, 텔레메트리)를 실행하려면route_turn을 오버라이드하세요:
route_turn에서 비어 있지 않은 문자열을 반환하세요. falsy 값을 반환해도 오버라이드에서 _route_with_config()가 호출되지는 않습니다. 대신 현재 턴의 사전 분류된 intent, 설정된 경우 사용 중단된 answer_from_history 호환 경로, 마지막으로 converse 순으로 fallback합니다. 이전 턴의 last_intent는 router 컨텍스트에서 사용할 수 있지만 fallback으로 다시 실행되지는 않습니다.
append_assistant_message와 append_agent_result
@listen(label) 핸들러 안에서 두 가지 중 선택하세요:
self.append_assistant_message(text)— 사용자에게 보이는 어시스턴트 턴을state.messages에 추가합니다. 다음 턴의converse_turn이 이 내용을 보게 됩니다.self.append_agent_result(agent_name, result, visibility="private")— 구조화된 이벤트를state.events에, 스레드를state.agent_threads[agent_name]에 기록합니다. public 가시성은 자동으로append_assistant_message도 호출합니다. 정식 히스토리를 더럽히지 말아야 할 임시 작업에는 private을 쓰세요.
ConversationConfig.visible_agent_outputs로 특정 에이전트의 private 결과를 전역적으로 public으로 승격할 수 있습니다 ("all" 또는 이름 리스트).
JSON/YAML로 대화형 플로우 선언하기
선언적 Flow도 대화형으로 만들 수 있습니다. 최상위conversational 블록을 추가하고 라우트 레이블을 listen하는 메서드로 자체 라우트를 선언하세요:
enabled의 기본값은 true입니다. 설정은 유지하면서 채팅을 끄려면 enabled: false로 지정하세요. 이 경우 내장 메서드 합성도 비활성화되므로 선언에 일반 비대화형 그래프를 제공해야 합니다.
세 가지가 자동으로 제공됩니다:
선언적
llm, router.llm, intent_llm 필드는 모델 id 또는 {model: openai/gpt-4o-mini, max_tokens: 512} 같은 설정 mapping을 받습니다. conversational 블록은 default_intents, visible_agent_outputs, defer_trace_finalization과 위에 나온 RouterConfig 필드도 지원합니다. 사용 중단된 answer_from_history_prompt / answer_from_history_llm 선언은 호환성을 위해 계속 허용됩니다.
클래스 기반 대화형 플로우와 동일한 턴 API로 Python에서 실행합니다:
라우트 이름 짓기
라우트 레이블과 메서드 이름은 하나의 트리거 네임스페이스를 공유하므로, 핸들러 이름이 자신이 listen하는 라우트와 같으면 안 됩니다 —create_video가 create_video를 listen하면 플로우 생성 시 거부됩니다. handle_* 접두사를 사용하세요.
선언으로 표현할 수 없는 것
crewai run은 선언적 대화형 Flow에 대해 Python 대화형 Flow와 같은 채팅 TUI를 엽니다. 채팅 루프에는 터미널이 필요하므로 headless 실행은 단일 턴을 실행하는 대신 안내와 함께 0이 아닌 코드로 종료됩니다. 이런 환경에서는 Python의 handle_turn() 또는 stream_turn()으로 실행하세요. human_feedback: 블록이 있는 선언적 메서드(Python: @human_feedback)는 터미널 REPL에서 실행됩니다. 런타임이 TUI가 처리할 수 없는 블로킹 prompt로 feedback을 수집하기 때문입니다. 대화형 Flow에서는 --inputs를 받지 않습니다. 각 턴의 입력은 사용자가 입력하는 메시지이며 id로 세션을 재개하는 기능은 아직 CLI에 연결되지 않았습니다. 필요하면 Python에서 flow.handle_turn(message, session_id=...)을 사용하세요.
턴 간 트레이싱
defer_trace_finalization=True (ConversationConfig 기본값):
- 채팅 세션 전체에 하나의 trace batch.
- 첫 턴에만
flow_started;finalize_session_traces()에서flow_finished한 번. - 턴별
kickoff는 “Trace batch finalized”를 출력하지 않음. - 중첩 작업 (
Agent.kickoff(), crew, Exa tool)은 부모 batch에 추가; 내부AgentExecutorflow가 세션 batch를 조기 종료하지 않음.
flow.chat()이 finalize_session_traces()를 대신 호출합니다. handle_turn()으로 직접 루프를 소유하는 경우 세션이 끝날 때 finalize_session_traces()를 호출하세요.
suppress_flow_events=True는 Rich 콘솔 패널을 숨기고 메서드 실행 이벤트를 억제합니다. Flow start/finish 이벤트는 계속 발생하므로 바깥쪽 Flow 수명 주기는 추적할 수 있지만 개별 메서드 span은 생략됩니다.
대화형 Flow trace 수명 주기
대화형 Flow는 동일한 tracing 수명 주기를 따릅니다. defer_trace_finalization 기본값이 True이므로 각 handle_turn()은 세션 trace를 열린 상태로 유지합니다. 지연된 턴은 턴별 flow_failed도 억제합니다. 턴 오류나 세션 중단이 발생하면 세션을 명시적으로 finalize하세요. 그러면 턴별 FlowFailed 이벤트 대신 세션 수준 FlowFinished 이벤트로 batch가 닫힙니다. REPL/루프는 항상 try/finally로 감싸고 종료 시 flow.finalize_session_traces()를 호출하세요. 호출하지 않으면 trace batch가 열린 채 남아 최종 대화가 export되지 않을 수 있습니다.
스트리밍
대화형 UI에서는stream_turn()을 사용하고 순서가 보장된 StreamFrame 객체를 순회하세요:
stream = True로 설정하면 kickoff()가 StreamSession을 반환합니다. handle_turn()을 사용할 때 flow.stream = True로 설정하지 마세요. 대화형 스트리밍 수명 주기는 stream_turn()이 관리합니다.
