스트리밍 응답 소비하기

스트리밍 응답 소비하기

생성 엔드포인트는 완성된 응답 하나를 돌려주거나, 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용 pingdata: 페이로드 없이 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.

전형적인 최소 처리는 이렇게 돼요.

  1. 답변 조각을 순서대로 이어 붙인다.

    • Chatbot·Chatflow 앱은 message 이벤트
    • Legacy Agent·Agent 앱은 agent_message 이벤트

    Agent 앱의 경우 마지막에 message 이벤트 하나가 전체 답변을 반복해서 실어 와요. 이걸 추가로 붙일 텍스트가 아니라 최종 답변으로 취급하세요.

  2. 올바른 종료 이벤트에서 닫는다.

    • Chatbot, Legacy Agent, Agent 앱은 message_end
    • Chatflow 앱은 message_end 다음 workflow_finished(둘 다 그 순서로 온다)
    • Workflow 앱은 workflow_finished
  3. error를 표면화한다.

스트림 도중 오류 처리

스트림이 열린 뒤의 실패는 HTTP 상태를 바꾸지 않아요. 연결은 계속 200이에요. 실패가 어디서 나느냐에 따라 표면화 방식이 달라집니다.

  • 워크플로 노드 실패status: "failed"를 담은 node_finishedworkflow_finished 이벤트로 도착해요.
  • 그 외 실패status, code, message를 담은 error 이벤트로 스트림을 끝내요.

둘 다 처리하고, 어느 쪽이든 그 요청의 종료로 취급하세요.

재연결과 재개

자꾸 헷갈리기 쉬운 식별자가 두 개 있는데요.

둘 다 스트림 자체에서 도착해요. 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 자체는 건너뛰는 것 외에 별도 처리가 필요 없어요.

더 알아보기 (Learn more)