스트리밍 (Streaming)
스트리밍 (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 = flow.stream_events(inputs={"topic": "AI agents"})
세션은 이터레이터이자 최종 결과를 담는 보관소 역할을 동시에 해요.
with stream:
for frame in stream:
print(frame.content, end="", flush=True)
result = stream.result
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는 스트림 소비가 끝난 뒤에만 읽을 수 있고, 채널 투영은 화면에서 그려주는 영역을 나눌 때만 쓰면 돼요. 스트림 자체는 항상 하나의 순서 있는 시간축이니까요.
그리고 런타임 통합이나 대화형 인터페이스를 새로 만든다면 크루 청크 스트리밍보다 프레임 스트리밍을 기본으로 잡는 게 좋아요. 이벤트 구조가 하나로 일관되게 유지되거든요.