기술

RAG 처리 과정을 화면에 띄웠더니, 디버깅 도구가 됐다

2026.03.317분 읽기

질문을 던졌는데, 화면이 멈춰 있다

RAG 채팅을 만들고 나서 직접 써보는데, 질문을 입력하면 화면에 아무것도 안 나왔거든요.
답변이 나오기까지 짧으면 5초, 길면 10초 넘게 걸리는데 그 사이에 화면은 그냥 비어 있었습니다.

"지금 돌아가고 있는 건가?" 하는 생각이 매번 들더라고요.
브라우저가 멈춘 건지, 서버가 죽은 건지, 아니면 원래 오래 걸리는 건지 구분이 안 됐습니다.

로딩 스피너라도 하나 달아둘걸, 하는 생각이 계속 들었는데요.
스피너만으로는 "지금 뭘 하고 있는지"를 알 수가 없었습니다.


사용자만 불안한 게 아니었다

사실 이게 단순히 UX 문제로 끝나는 건 아니었거든요.

개발하면서 RAG 파이프라인을 수정할 때마다, 의도 분석이 제대로 동작하는지, 검색이 실행됐는지, 리랭크까지 정상으로 돌아갔는지를 매번 확인해야 했는데요.


그 확인 수단이 Python 서버 로그뿐이었습니다.

터미널을 열어두고, 질문을 던지고, 로그를 눈으로 훑어서 각 단계가 정상인지 체크하는 방식이었거든요.
채팅 화면에서는 최종 답변만 나오니까, 중간 과정에서 뭐가 빠졌는지, 어디서 느려졌는지를 바로 알 수가 없었습니다.

이전에 파이프라인 구간별 타이밍을 찍어봤을 때도 의도 분석 단계가 병목이라는 걸 로그를 열어봐야 알 수 있었는데요.


"이걸 화면에서 바로 보면 안 되나?" 하는 생각이 계속 남아 있었습니다.

1.jpeg


invoke()와 stream()의 차이

왜 중간 과정이 안 보였는지, 원인은 LangGraph의 실행 방식에 있었습니다.

LangGraph로 RAG 파이프라인을 구성하면 기본적으로 invoke()를 써서 전체를 한 번에 실행하거든요.
의도 분석 → 검색 → 리랭크 → 답변 생성까지 전부 끝난 뒤에야 결과를 돌려줍니다.

비유하자면, 택배를 시켰는데 추적 번호가 없는 상태예요.
"발송됨"인지 "배송 중"인지 "배달 완료"인지 알 수 없고, 어느 순간 갑자기 택배가 도착하는 거죠.

stream()은 추적 번호가 있는 택배입니다.

파이프라인을 노드 단위로 쪼개서 실행하면서, 각 노드가 끝날 때마다 이벤트를 보내줄 수 있거든요.

"의도 분석 완료" → "검색 시작" → "리랭크 완료" — 이런 식으로 중간 과정을 실시간으로 프론트엔드에 전달할 수 있게 됩니다.

핵심은, 기존에 마지막 답변 생성 단계에서만 가능했던 스트리밍을, 파이프라인 전체로 확장하는 거였습니다.

2.jpeg


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 스트리밍처럼 글자가 하나씩 나오는 방식이에요.

SCR-20260331-itqp.png

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

3.jpeg


처리 과정이 보이니까, 검증이 된다

이렇게 바꾸고 나서 가장 체감이 컸던 건 디버깅이었습니다.

RAG 파이프라인을 커스텀할 때마다, 수정한 부분이 제대로 반영됐는지 확인하는 게 일이었거든요.
예전에는 터미널을 켜서 Python 로그를 한 줄씩 읽어야 했는데, 이제는 채팅 화면에서 바로 보입니다.

"의도 분석은 통과했는데 검색 에이전트에서 시간이 오래 걸리네" — 이런 판단이 화면만 보고 가능해졌어요.

단순히 "기다려 주세요" 로딩 스피너를 보여주는 것과는 다른 차원이었습니다.
로딩 스피너는 "뭔가 돌아가고 있다"만 알려주지만, step 표기는 "지금 6단계 중 3단계에 있다"를 알려주거든요.

사용자 입장에서도 "지금 AI가 검색하고 있구나", "이제 답변을 만들고 있구나" 하고 과정이 눈에 들어오니까, 기다리는 동안의 불안감이 확 줄었고요.


개발자 입장에서는 터미널 없이도 파이프라인이 의도한 순서대로 돌아가는지 바로 확인할 수 있게 됐습니다.

결국 "사용자에게 뭘 보여줄까"를 고민하다가, 개발 과정의 검증 도구까지 얻은 셈이더라고요.


마무리

처음에는 빈 화면이 보기 싫어서 시작한 작업이었는데, 결과적으로 사용자 경험과 개발 편의성을 동시에 챙기게 됐습니다.

핵심은 LangGraph의 실행 방식을 invoke()에서 stream()으로 바꾸는 것이었고, 각 노드의 실행 상태를 SSE로 프론트에 전달하는 구조를 잡는 것이었습니다.

RAG 파이프라인을 운영하면서 "로그 안 열고 중간 과정을 확인하고 싶다"는 생각이 든다면, step 이벤트 스트리밍을 검토해보는 걸 추천합니다.

#RAG#LangGraph#스트리밍#SSE#처리과정표시#UX#AI전환기