스트리밍 (Streaming)¶
개념¶
스트리밍은 작업이 아직 진행 중인 동안에도 애플리케이션이 실행 업데이트를 받아볼 수 있게 해주는 방식이에요. 최종 결과를 기다리는 대신, LLM 토큰·도구 활동·Flow 생명주기 이벤트·대화 메시지가 발생하는 그대로 렌더링할 수 있어요.
CrewAI는 스트리밍 표면(surface)이 두 가지예요.
| 표면 | 사용처 | 출력 |
|---|---|---|
| 프레임 스트리밍 (Frame streaming) | Flow, 직접 LLM 호출, 대화 턴 | 순서가 보장된 StreamFrame 객체 |
| 크루 청크 스트리밍 (Crew chunk streaming) | stream=True를 켠 Crew |
CrewStreamingOutput 청크 |
새 런타임 통합, UI, 터미널 앱, 서비스 브리지, 대화형 인터페이스에는 프레임 스트리밍을 쓰세요. 런타임 전반에 걸쳐 이벤트 구조가 하나로 안정적으로 유지되기 때문이에요.
StreamFrame¶
StreamFrame은 스트리밍이 가능한 런타임이 내보내는 공통 객체예요.
frame.id # 고유한 프레임 id
frame.seq # 실행 로컬 순서 (가능할 때)
frame.type # 소스 이벤트 타입. 예: "llm_stream_chunk"
frame.channel # "llm", "flow", "tools", "messages", "lifecycle", "custom"
frame.namespace # 소스/런타임 네임스페이스
frame.timestamp # 이벤트 타임스탬프
frame.parent_id # 부모 이벤트 id (가능할 때)
frame.previous_id # 이전 이벤트 id (가능할 때)
frame.data # 구조화된 이벤트 페이로드
frame.event # frame.data의 별명
frame.content # 토큰류 프레임의 출력 가능 텍스트, 아니면 ""
대부분의 소비자가 실제로 쓰는 필드는 이 정도예요.
| 필드 | 어디에 쓰나요 |
|---|---|
channel |
프레임을 올바른 UI 영역으로 라우팅 |
type |
채널 안에서 특정 이벤트 처리 |
content |
토큰류 텍스트 출력 |
event |
도구 이름·메시지 역할 같은 구조화 메타데이터 읽기 |
seq |
실행 순서 보존 |
채널 (Channels)¶
프레임은 상위 수준의 채널로 묶여요.
| 채널 | 담는 내용 |
|---|---|
llm |
LLM 호출 생명주기, 텍스트 청크, thinking 청크 |
flow |
Flow 생명주기, 메서드 실행, 라우팅, pause·resume 이벤트 |
tools |
도구 사용 시작·종료·오류 이벤트 |
messages |
대화 기록(transcript) 이벤트 |
lifecycle |
다른 채널에 속하지 않는 런타임 생명주기 이벤트 |
custom |
내장 채널에 매핑되지 않는 이벤트 |
스트림 자체는 하나의 순서 있는 시간축으로 유지돼요. 채널 투영(projection)은 그 시간축의 일부만 보고 싶은 소비자를 위한 것이라고 보면 돼요.
스트림 세션 (Stream Sessions)¶
프레임 스트리밍은 스트림 세션을 반환해요.
세션은 이터레이터이자 최종 결과를 담는 보관소 역할을 동시에 해요.
stream.result를 읽기 전에 반드시 스트림을 소비해야 해요. 너무 일찍 결과를 읽으면 런타임이 아직 프레임을 만들고 있을 수 있기 때문에 오류가 나요.
코드 스니펫¶
특정 채널만 필요할 때 (채널 투영)¶
with flow.stream_events(inputs={"topic": "AI agents"}) as stream:
for frame in stream.llm:
print(frame.content, end="", flush=True)
result = stream.result
사용 가능한 투영은 이렇게 정리돼요.
| 투영 | 프레임 |
|---|---|
stream.events |
모든 프레임 |
stream.llm |
LLM 프레임 |
stream.flow |
Flow 프레임 |
stream.tools |
도구 프레임 |
stream.messages |
대화 메시지 프레임 |
stream.interleave([...]) |
선택한 채널을 상대 순서로 |
스트리밍할 런타임에 맞는 진입점 (Entrypoints)¶
| 런타임 | 스트리밍 진입점 |
|---|---|
| Flow | flow.stream_events(...) |
stream=True를 켠 Flow |
flow.kickoff(...) 이 스트림 세션 반환 |
| 비동기 Flow | flow.astream(...) 또는 stream=True일 때 await flow.kickoff_async(...) |
| 직접 LLM 호출 | llm.stream_events(...) |
| 대화형 Flow 턴 | flow.stream_turn(...) |
| Crew | Crew(..., stream=True).kickoff(...) 이 CrewStreamingOutput 반환 |
실무 관점¶
직접 llm.call(...)을 쓰면 여전히 최종 조합된 LLM 결과를 반환해요. LLM 청크가 도착하는 대로 순회하고 싶다면 llm.stream_events(...)를 써야 해요.
두 가지를 헷갈리기 쉬워요. stream.result는 스트림 소비가 끝난 뒤에만 읽을 수 있고, 채널 투영은 화면에서 그려주는 영역을 나눌 때만 쓰면 돼요. 스트림 자체는 항상 하나의 순서 있는 시간축이니까요.
그리고 런타임 통합이나 대화형 인터페이스를 새로 만든다면 크루 청크 스트리밍보다 프레임 스트리밍을 기본으로 잡는 게 좋아요. 이벤트 구조가 하나로 일관되게 유지되거든요.