RAG 처리 과정을 화면에 띄웠더니, 디버깅 도구가 됐다
질문을 던졌는데, 화면이 멈춰 있다
RAG 채팅을 만들고 나서 직접 써보는데, 질문을 입력하면 화면에 아무것도 안 나왔거든요.
답변이 나오기까지 짧으면 5초, 길면 10초 넘게 걸리는데 그 사이에 화면은 그냥 비어 있었습니다.
"지금 돌아가고 있는 건가?" 하는 생각이 매번 들더라고요.
브라우저가 멈춘 건지, 서버가 죽은 건지, 아니면 원래 오래 걸리는 건지 구분이 안 됐습니다.
로딩 스피너라도 하나 달아둘걸, 하는 생각이 계속 들었는데요.
스피너만으로는 "지금 뭘 하고 있는지"를 알 수가 없었습니다.
사용자만 불안한 게 아니었다
사실 이게 단순히 UX 문제로 끝나는 건 아니었거든요.
개발하면서 RAG 파이프라인을 수정할 때마다, 의도 분석이 제대로 동작하는지, 검색이 실행됐는지, 리랭크까지 정상으로 돌아갔는지를 매번 확인해야 했는데요.
그 확인 수단이 Python 서버 로그뿐이었습니다.
터미널을 열어두고, 질문을 던지고, 로그를 눈으로 훑어서 각 단계가 정상인지 체크하는 방식이었거든요.
채팅 화면에서는 최종 답변만 나오니까, 중간 과정에서 뭐가 빠졌는지, 어디서 느려졌는지를 바로 알 수가 없었습니다.
이전에 파이프라인 구간별 타이밍을 찍어봤을 때도 의도 분석 단계가 병목이라는 걸 로그를 열어봐야 알 수 있었는데요.
"이걸 화면에서 바로 보면 안 되나?" 하는 생각이 계속 남아 있었습니다.

invoke()와 stream()의 차이
왜 중간 과정이 안 보였는지, 원인은 LangGraph의 실행 방식에 있었습니다.
LangGraph로 RAG 파이프라인을 구성하면 기본적으로 invoke()를 써서 전체를 한 번에 실행하거든요.
의도 분석 → 검색 → 리랭크 → 답변 생성까지 전부 끝난 뒤에야 결과를 돌려줍니다.
비유하자면, 택배를 시켰는데 추적 번호가 없는 상태예요.
"발송됨"인지 "배송 중"인지 "배달 완료"인지 알 수 없고, 어느 순간 갑자기 택배가 도착하는 거죠.
stream()은 추적 번호가 있는 택배입니다.
파이프라인을 노드 단위로 쪼개서 실행하면서, 각 노드가 끝날 때마다 이벤트를 보내줄 수 있거든요.
"의도 분석 완료" → "검색 시작" → "리랭크 완료" — 이런 식으로 중간 과정을 실시간으로 프론트엔드에 전달할 수 있게 됩니다.
핵심은, 기존에 마지막 답변 생성 단계에서만 가능했던 스트리밍을, 파이프라인 전체로 확장하는 거였습니다.

step 이벤트 설계와 프론트 연동
구현은 크게 백엔드와 프론트엔드 두 부분으로 나뉘었습니다.
백엔드: 각 노드에서 step 이벤트 전송
LangGraph의 stream() 모드로 전환한 뒤, 각 노드가 실행되기 전에 SSE(Server-Sent Events)로 step 이벤트를 보내는 구조입니다.
이벤트 자체는 단순하게 잡았거든요.
"지금 어떤 단계에 진입했는지"만 알려주면 충분했습니다.
단계 이름과 상태(시작/완료)만 담으면 프론트에서 체크리스트를 그릴 수 있었어요.
기존에 invoke()로 한 번에 실행하던 코드를 노드별로 쪼개는 게 가장 큰 작업이었고요.
LangGraph의 stream() 모드가 노드 단위 이벤트를 지원하기 때문에, 파이프라인 구조 자체를 바꿀 필요는 없었습니다.
프론트엔드: step 이벤트 파싱 + 체크리스트 UI
프론트에서는 SSE 스트림을 열어두고, step 이벤트가 들어올 때마다 체크리스트 UI를 업데이트합니다.
현재 6단계로 나눠서 보여주고 있어요.
순서 | 단계 | 역할 |
|---|---|---|
1 | 의도 분석 | 질문의 의도 파악 |
2 | 질의 분석 | 검색에 적합한 쿼리 생성 |
3 | 임베딩 생성 | 벡터 검색용 임베딩 변환 |
4 | 작업 분배 | 검색 대상 소스 결정 |
5 | 검색 에이전트 실행 | 실제 검색 수행 |
6 | 답변 생성 | LLM이 최종 답변 작성 |
각 단계가 완료되면 ✓ 표시가 붙고, 현재 진행 중인 단계에는 로딩 표시가 나옵니다.
마지막 답변 생성 단계에서는 일반적인 LLM 스트리밍처럼 글자가 하나씩 나오는 방식이에요.

여기서 중요한 건, 기존 답변 스트리밍과 step 이벤트를 같은 SSE 연결에서 처리한다는 점이었습니다.
이벤트 타입으로 구분하면 되거든요 — step 이벤트와 답변 chunk 이벤트를 분리해서 프론트에서 각각 다르게 처리합니다.

처리 과정이 보이니까, 검증이 된다
이렇게 바꾸고 나서 가장 체감이 컸던 건 디버깅이었습니다.
RAG 파이프라인을 커스텀할 때마다, 수정한 부분이 제대로 반영됐는지 확인하는 게 일이었거든요.
예전에는 터미널을 켜서 Python 로그를 한 줄씩 읽어야 했는데, 이제는 채팅 화면에서 바로 보입니다.
"의도 분석은 통과했는데 검색 에이전트에서 시간이 오래 걸리네" — 이런 판단이 화면만 보고 가능해졌어요.
단순히 "기다려 주세요" 로딩 스피너를 보여주는 것과는 다른 차원이었습니다.
로딩 스피너는 "뭔가 돌아가고 있다"만 알려주지만, step 표기는 "지금 6단계 중 3단계에 있다"를 알려주거든요.
사용자 입장에서도 "지금 AI가 검색하고 있구나", "이제 답변을 만들고 있구나" 하고 과정이 눈에 들어오니까, 기다리는 동안의 불안감이 확 줄었고요.
개발자 입장에서는 터미널 없이도 파이프라인이 의도한 순서대로 돌아가는지 바로 확인할 수 있게 됐습니다.
결국 "사용자에게 뭘 보여줄까"를 고민하다가, 개발 과정의 검증 도구까지 얻은 셈이더라고요.
마무리
처음에는 빈 화면이 보기 싫어서 시작한 작업이었는데, 결과적으로 사용자 경험과 개발 편의성을 동시에 챙기게 됐습니다.
핵심은 LangGraph의 실행 방식을 invoke()에서 stream()으로 바꾸는 것이었고, 각 노드의 실행 상태를 SSE로 프론트에 전달하는 구조를 잡는 것이었습니다.
RAG 파이프라인을 운영하면서 "로그 안 열고 중간 과정을 확인하고 싶다"는 생각이 든다면, step 이벤트 스트리밍을 검토해보는 걸 추천합니다.