에이전트는 일반 API보다 응답 시간이 길다. 고객 기록을 조회하고 정책을 검색하며 여러 결과를 종합하는 동안 화면이 멈춰 있으면 사용자는 요청이 처리 중인지 실패했는지 알 수 없다. 반대로 모델의 내부 reasoning을 그대로 스트리밍하면 민감한 문맥과 불안정한 중간 판단이 제품 계약이 된다.

이 프로젝트는 두 극단 사이에서 “governed progress event”를 전달한다. 사용자에게 유용한 진행 상황, 사용한 도구, 인용, 승인 요청과 최종 결과만 공개하고 private chain-of-thought는 보내지 않는다.

왜 WebSocket 대신 SSE인가

채팅처럼 보이지만 이 흐름의 실시간 방향은 대부분 서버에서 클라이언트로 향한다. 사용자는 한 번 HTTP 요청을 보내고 서버의 진행 이벤트를 받는다. 승인과 thread 조회는 별도의 일반 HTTP endpoint로 처리할 수 있다.

SSE를 선택한 이유는 단순하다.

  • 표준 HTTP 인프라와 잘 맞는다.
  • 서버에서 클라이언트로 순서 있는 text event를 보내기 쉽다.
  • 연결 수명주기와 프록시 설정이 WebSocket보다 단순하다.
  • 승인과 거절 같은 명령은 버전이 있는 REST 계약으로 분리할 수 있다.

양방향 저지연 상호작용이나 빈번한 클라이언트 이벤트가 필요해지면 WebSocket을 검토할 수 있다. 현재 요구에는 더 큰 통신 장치를 먼저 넣을 이유가 없다.

공개하는 이벤트를 제한한다

프런트엔드가 이해하는 이벤트는 제품 의미를 가진다.

  • progress: 조사 단계가 진행 중임을 알린다.
  • tool: 어떤 허용된 업무 도구가 완료됐는지 보여준다.
  • citation: 정책 근거의 ID, 버전과 출처를 전달한다.
  • approval_required: 실행 전 확인할 도구 인자를 보낸다.
  • final: 사용자에게 공개할 최종 답변이다.
  • error: 안전하게 요약된 실패를 전달한다.

에이전트 런타임은 message state에서 ToolMessage와 최종 AIMessage를 구분한다. tool activity에는 도구 이름과 성공 여부만 담고 원시 고객 레코드나 프롬프트 전문을 진행 이벤트로 흘리지 않는다.

취소도 실행 계약의 일부다

사용자가 탭을 닫거나 연결을 끊으면 SSE generator의 cancellation을 하위 작업으로 전달해야 한다. 그렇지 않으면 아무도 기다리지 않는 모델 호출과 검색이 계속 자원을 사용한다. 반대로 이미 시작된 민감한 쓰기를 단순 연결 종료만으로 취소했다고 가정해서도 안 된다. 쓰기 상태는 checkpoint와 idempotency를 통해 별도로 확인해야 한다.

UI는 이벤트를 설명으로 바꾼다

프런트엔드는 내부 클래스 이름이나 전체 JSON을 노출하지 않는다. “Records MCP completed” 같은 제한된 상태, 확인 가능한 citation, 승인할 실제 인자를 사용자 문맥에 맞게 표시한다. 진행 표시는 기술 디버그 로그가 아니라 제품 인터페이스다.

좋은 스트리밍 경험은 토큰이 한 글자씩 나타나는 효과에만 있지 않다. 사용자가 지금 무엇을 기다리는지 알고, 중요한 변경 앞에서 멈출 수 있으며, 최종 답변의 근거를 확인할 수 있어야 한다. SSE는 그 계약을 전달하는 작은 운송 수단일 뿐이다.