스트리밍 응답 소비하기
스트리밍 응답 소비하기
생성 엔드포인트는 완성된 응답 하나를 돌려주거나, Server-Sent Events(SSE) 스트림을 돌려줘요. 어느 쪽인지는 요청할 때 response_mode로 고릅니다. 보통은 스트리밍을 쓰는 게 좋아요. 답변이 생성되는 대로 렌더링되고, 긴 실행도 중간에 잘리지 않으니까요.
출처: 공식문서
응답 모드 고르기
blocking은 생성이 끝나면 JSON 본문 하나를 돌려줘요. 짧고 상호작용이 없는 호출에선 더 단순한 통합이지만, 긴 생성은 중단 위험이 있어요. 프록시가 긴 요청을 끊기도 하고, Dify Cloud에서는 업스트림 응답이 타임아웃 안에 오지 않으면 edge proxy가 연결을 끊을 수 있거든요.
streaming은 답변을 SSE 이벤트로 전달해요. 사용자에게 보여줄 일이라면, 긴 실행이라면, 그리고 Human Input으로 멈추는 모든 흐름이라면 이 모드를 쓰세요.
💡 Legacy Agent와 Agent 앱은 스트리밍만 지원해요.
스트림 파싱
ping을 제외한 모든 이벤트는 빈 줄로 끝나는 data: 줄 하나에 JSON 객체 하나를 실어 와요.
event 필드를 읽어 뭘 할지 정하고, data: 로 시작하지 않는 줄은 건너뛰면 돼요. keep-alive용 ping은 data: 페이로드 없이 event: ping 줄만 오거든요.
Chatflow와 Workflow 앱 스트림은 맨 첫 프레임으로 ping을 보내고, 실행 중 약 10초마다 더 온다. 첫 프레임이 아니라 첫 data: 이벤트(workflow_started)를 "실행이 받아들여졌다"는 신호로 봐야 해요.
import json
import requests
body = {
"query": "What are this month's top issues?",
"inputs": {},
"user": "customer-4821",
"response_mode": "streaming",
}
with requests.post(url, headers=headers, json=body, stream=True) as r:
for line in r.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue # skips blank separators and ping lines
event = json.loads(line[len("data: "):])
handle(event)
실제 네트워크에서는 스트림이 이렇게 생겼어요.
event: ping
data: {"event": "workflow_started", "task_id": "c3800678-…", "workflow_run_id": "fb47b2e6-…", "data": {…}}
data: {"event": "node_finished", "task_id": "c3800678-…", "workflow_run_id": "fb47b2e6-…", "data": {…}}
💡 curl로 테스트할 땐
-N(--no-buffer)을 넘겨서, 실행이 끝난 뒤가 아니라 도착하는 대로 이벤트가 출력되게 하세요.
이벤트 유형별 분기
어떤 이벤트가 오는지는 앱 유형에 따라 달라요. 각 엔드포인트의 이벤트 표를 계약으로 삼으세요: Send Chat Message, Run Workflow, Send Completion Message.
전형적인 최소 처리는 이렇게 돼요.
-
답변 조각을 순서대로 이어 붙인다.
- Chatbot·Chatflow 앱은
message이벤트 - Legacy Agent·Agent 앱은
agent_message이벤트
Agent 앱의 경우 마지막에
message이벤트 하나가 전체 답변을 반복해서 실어 와요. 이걸 추가로 붙일 텍스트가 아니라 최종 답변으로 취급하세요. - Chatbot·Chatflow 앱은
-
올바른 종료 이벤트에서 닫는다.
- Chatbot, Legacy Agent, Agent 앱은
message_end - Chatflow 앱은
message_end다음workflow_finished(둘 다 그 순서로 온다) - Workflow 앱은
workflow_finished
- Chatbot, Legacy Agent, Agent 앱은
-
error를 표면화한다.
스트림 도중 오류 처리
스트림이 열린 뒤의 실패는 HTTP 상태를 바꾸지 않아요. 연결은 계속 200이에요. 실패가 어디서 나느냐에 따라 표면화 방식이 달라집니다.
- 워크플로 노드 실패는
status: "failed"를 담은node_finished와workflow_finished이벤트로 도착해요. - 그 외 실패는
status,code,message를 담은error이벤트로 스트림을 끝내요.
둘 다 처리하고, 어느 쪽이든 그 요청의 종료로 취급하세요.
재연결과 재개
자꾸 헷갈리기 쉬운 식별자가 두 개 있는데요.
task_id는 진행 중인 생성을 제어해요. 중지 엔드포인트(Stop Chat Message Generation / Stop Workflow Task)가 받는 값이에요.workflow_run_id는 영속적 실행 레코드의 이름이에요.
둘 다 스트림 자체에서 도착해요. error를 제외한 모든 이벤트가 task_id를 싣고, 워크플로·노드 이벤트는 workflow_run_id도 실어요. workflow_run_id는 도착하는 즉시 저장해 두세요. 도중에 연결이 끊기면 재연결하거나 결과를 확인할 수 있는 유일한 핸들이라서요.
워크플로 기반 실행(Workflow·Chatflow 앱)에선 연결이 끊겨도 치명적이지 않아요. Stream Workflow Events로 스트림을 다시 열되, workflow_run_id와 실행을 시작했던 것과 **같은 user**를 넘기세요. 불일치하면 404가 나요.
include_state_snapshot=true를 추가하면 이미 실행된 노드의 상태를 먼저 재생해 주고, continue_on_pause=true는 여러 번의 Human Input 일시정지를 넘나들며 스트림 하나를 계속 열어두는 데 쓰여요.
여전히 실행 중인 워크플로에 재연결했다면, 재연결된 스트림의 마지막 이벤트만 믿지 말고 Get Workflow Run Detail로 완료를 확인하세요.
나머지 응답에는 재개 엔드포인트가 없어요. 답변 도중 연결이 끊기면 새 요청을 내면 됩니다. 채팅 성격 앱이라면 List Conversation Messages로 대화에 저장된 걸 확인할 수 있어요.
연결 유지하기
이벤트 사이의 여유 시간에 연결이 죽지 않도록, 클라이언트의 읽기 타임아웃을 10초 ping 간격보다 넉넉하게 잡으세요. ping 자체는 건너뛰는 것 외에 별도 처리가 필요 없어요.