개요
대화형 앱은 각 사용자 입력을 동일한 세션 id로 새 flow 실행으로 처리합니다. CrewAI는 메시지 기록, 선택적 의도 분류, 지연 트레이싱, UI 브리지, 그리고 대화형 flow용 로컬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은 다음 파이프라인을 실행합니다:
_configure_conversational_kickoff—session_id/user_message를inputs에 병합,ConversationalConfig적용, 설정 시 지연 트레이싱 활성화.- 상태 복원 —
inputs["id"]가 있고@persist가 설정되면 최신 스냅샷 로드. FlowStarted— 지연 세션의 첫 턴에서만 발생.prepare_conversational_turn— 사용자 메시지를state.messages에 추가,last_user_message설정,last_intent초기화,intents/default_intents+intent_llm설정 시 분류.- 그래프 실행 —
@start→@router→@listen핸들러. - 실행 종료 — 지연 활성화 시 턴별
flow_finished및 trace 종료 건너뜀; 중첩Agent.kickoff()/ crew도 부모 batch를 닫지 않음.
append_assistant_message(reply)**를 호출해 다음 턴의 conversation_messages에 어시스턴트 응답이 포함되게 하세요. 사용자 입력은 handle_turn이 이미 저장합니다 — 핸들러에서 다시 추가하지 마세요.
ConversationalConfig (클래스 수준 기본값)
Flow 서브클래스에 conversational_config: ClassVar[ConversationalConfig | None]로 설정합니다.
intents= 및 intent_llm= 키워드로 kickoff마다 재정의할 수 있습니다.
ChatState (권장 persist 형태)
ConversationalInputs는 kickoff(inputs={...})용 TypedDict: id, user_message, last_intent.
Flow 대화 API
kickoff / kickoff_async 파라미터
인스턴스 속성
메서드 및 프로퍼티
모듈 헬퍼 (crewai.flow.conversation)
테스트 또는 커스텀 오케스트레이션용:
의도 라우팅 패턴
A. ConversationalConfig로 사전 분류 (가장 단순)
default_intents와 intent_llm 설정. 각 kickoff가 @router 전에 분류; route()에서 self.state.last_intent 읽기.
B. @router 내부에서 분류 (풍부한 프롬프트)
default_intents=None으로 kickoff는 메시지만 추가. route()에서 커스텀 프롬프트로 classify_intent 호출:
@listen("RESEARCH") 등에서 Agent.kickoff()와 tool 사용 — 단순 LLM.call() 대신.
flow가 끝났지만 사용자는 계속 대화할 때
FlowFinished는 이번 그래프 실행이 완료됨을 의미합니다. 같은 session_id로 또 다른 kickoff로 대화가 이어집니다. @persist가 messages, 플래그, 컨텍스트를 복원합니다.
Persist 패턴: 전체 Flow 클래스보다 단일 종료 스텝(예: finalize)에 @persist를 두는 것이 좋습니다. 클래스 수준 persist는 매 메서드 후 저장하며, load_state는 최신 행을 사용해 같은 턴의 핸들러 업데이트를 놓칠 수 있습니다.
후속 채팅 줄에 @human_feedback를 쓰지 마세요. 특정 스텝 출력을 사람이 승인해야 할 때만 사용하세요.
대화형 Flow (실험적)
Flow 서브클래스에 conversational = True를 지정하면 대화형 챗 그래프가 활성화됩니다. 베이스 Flow가 @start / @router / converse_turn / end_conversation 그래프를 노출하고, state.messages를 관리하며, router LLM을 구동하고, 턴 간 trace 배치를 열린 상태로 유지합니다. 여러분은 커스텀 라우트만 작성하면 되고, 나머지는 프레임워크가 담당합니다.
LLM 기반 라우터와 라우트별 핸들러로 멀티턴 챗을 만들고 싶지만 라이프사이클을 직접 배선하고 싶지 않을 때 사용하세요. 완전한 제어가 필요하면 위의 Flow[ChatState]로 내려가세요.
빠른 예제
chat()을 사용하세요:
chat()은 handle_turn()을 REPL로 감싸고, exit / quit에서 종료하며, 기본적으로 빈 줄을 건너뛰고, 세션이 끝날 때 finalize_session_traces()를 호출합니다.
ConversationConfig
클래스 단위의 챗 기본값을 부착하는 클래스 데코레이터입니다.
RouterConfig와 자동 생성되는 라우트 카탈로그
RouterConfig.route_descriptions[label]— 명시적 오버라이드.Flow.builtin_route_descriptions[label]—converse,end,answer_from_history용 프레임워크 캐닝 텍스트 (router LLM용으로 다듬어진 문구).@listen(label)핸들러 docstring의 첫 줄(비어있지 않은 줄).- 빈 문자열 (라우트만 카탈로그에 등장하고 설명은 없음).
@listen("X") + 한 줄짜리 docstring입니다:
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이 신호로 활용할 수 있습니다. conversation_start→route_conversation→ 선택된@listen핸들러 순으로 실행됩니다.- router는 결정을
state.last_intent에 저장합니다 (다음 턴의 router 컨텍스트에서 보입니다). - 핸들러가 문자열을 반환했지만
append_assistant_message를 직접 호출하지 않았다면,handle_turn이 대신 추가해 줍니다.
handle_turn()을 호출하세요. kickoff(inputs={"id": ...})를 직접 호출하면 대화형 턴 래퍼 없이 flow 그래프가 실행됩니다.
로컬 REPL용 chat()
flow.chat()은 handle_turn() 위에 얹은 바로 쓸 수 있는 터미널 래퍼입니다:
- 사용자 메시지를 입력받습니다.
exit/quit,EOFError,KeyboardInterrupt에서 멈춥니다.handle_turn(message, session_id=...)를 호출합니다.- 어시스턴트 결과를 출력합니다.
finally블록에서 지연된 세션 trace를 finalize합니다.
handle_turn()을 직접 사용하세요.
커스텀 router 동작
매 라우팅 결정마다 사이드 이펙트(이벤트 버스 셋업, 텔레메트리)를 실행하려면route_turn을 오버라이드하세요:
route_turn에서 문자열을 반환하세요. None을 반환하면 _route_with_config(...)로 떨어집니다.
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" 또는 이름 리스트).
턴 간 트레이싱
defer_trace_finalization=True (ConversationalConfig 기본값):
- 채팅 세션 전체에 하나의 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()이나 kickoff(...)로 직접 루프를 소유하는 경우, 세션이 끝날 때 finalize_session_traces()를 호출하세요.
suppress_flow_events=True는 Rich 콘솔 패널만 숨깁니다. trace 및 method 이벤트는 계속 발생합니다.
대화형 Flow trace 수명 주기
실험적 대화형 Flow는 동일한 tracing 수명 주기를 따릅니다. defer_trace_finalization 기본값이 True이므로 각 handle_turn()이 세션 trace를 열어 둡니다. 세션 끝에서 항상 finalize하세요 — REPL/루프를 try/finally로 감싸고 종료 시 flow.finalize_session_traces()를 호출하세요. 호출하지 않으면 batch가 열린 채 남아 마지막 대화가 export되지 않을 수 있습니다.
스트리밍
Flow 클래스에 stream = True. kickoff(...)가 표준 이벤트 버스를 통해 assistant_delta 등 이벤트를 발생시킵니다.
import
참고
- Flow 상태 관리 마스터하기
- 첫 Flow 만들기
- 데모:
lib/crewai/runner_conversational_flow_simple.py
