본문으로 건너뛰기

LangGraph 챗봇 스트리밍 아키텍처

2026년 5월 25일, Lumie는 챗봇 streaming을 위해 worker-local LangGraph 아키텍처를 선택했습니다. 첫 production version에서는 queue-driven chat path, Redis resumable stream, LangGraph Server를 채택하지 않았습니다.

핵심 요구사항은 좁지만 중요했습니다. 원장이 browser tab을 닫아도 분석 작업이 사라지면 안 됩니다.

배경

챗봇은 기존 경계를 보존해야 했습니다.

  • frontend SSE contract는 token, pending, done, error event로 고정됩니다.
  • monolith는 conversation, assistant message, pending action의 system of record로 남습니다.
  • tenant-scoped SQL은 Python이 아니라 RLS 아래의 lumie-backend에서 실행됩니다.
  • Python worker는 LangGraph execution과 langgraph schema의 checkpoint state만 소유합니다.
  • write tool은 실행 전에 HITL confirmation이 필요합니다.

현재 코드도 이 분리를 반영합니다.

  • lumie-worker/services/chatbot/src/usecase.py는 graph output을 SSE event로 매핑하고 final text를 backend callback으로 저장합니다.
  • lumie-worker/services/chatbot/src/graph/agent.py는 write tool을 interrupt()로 멈추고 Command(resume=...)로 재개합니다.
  • lumie-backend/modules/ai/src/main/java/com/lumie/ai/application/service/ChatService.java@Transactional 아래에서 conversation persistence를 소유합니다.
  • lumie-worker/services/chatbot/main.py는 FastAPI lifespan에서 compiled graph와 checkpointer를 wiring합니다.

검토한 선택지

선택지해결하는 문제최종 선택이 아니었던 이유
request-bound SSE only가장 단순한 live token deliveryclient disconnect가 run을 취소합니다
detached SSE producer plus persisted resultfrontend contract 변경 없이 tab-close safety 제공선택한 방식입니다
Redis resumable streams진행 중인 token 재생과 재연결아직 요청되지 않은 요구사항을 위해 infra와 frontend reconnect 작업이 늘어납니다
MQ-driven chat execution더 강한 job durabilitylow-latency token streaming에 맞지 않고 write-tool duplicate risk가 큽니다
LangGraph Servermanaged durable run과 resumable streamLumie의 self-hosted posture와 원본 결정 기록의 제약 때문에 제외했습니다

결정은 너무 일찍 섞이기 쉬운 세 관심사를 분리했습니다.

관심사선택한 소유자
pod가 살아 있는 동안의 executionusecase.py의 detached background task
live deliveryChatbotClient와 backend stream endpoint를 통한 SSE proxy
durable conversation resultbackend save_message, save_pending_action callback

선택한 아키텍처

usecase.py의 대표 runtime shape는 다음입니다.

task = asyncio.create_task(
self._run_graph(graph, inputs, config, conversation_id, req, queue, consumer_gone)
)

try:
while True:
item = await asyncio.wait_for(queue.get(), timeout=1.0)
...
finally:
consumer_gone.set()

HTTP response는 queue를 drain합니다. Graph run 자체는 detached되어 있으므로 client disconnect는 live delivery를 멈추지만 execution이나 final persistence를 자동으로 멈추지는 않습니다.

Graph boundary는 명시적입니다.

  1. scope_check는 명백한 out-of-scope 또는 prompt-injection request를 거부합니다.
  2. agent는 직접 답변할지 tool을 호출할지 결정합니다.
  3. tools는 read tool을 backend API로 실행하거나 write tool을 interrupt()로 멈춥니다.
  4. confirm_tool()은 confirm 또는 reject 이후 checkpointed run을 재개합니다.

Lumie에 맞았던 이유

선택한 설계는 가능한 platform option보다 의도적으로 작았습니다.

  • frontend와 backend streaming contract를 보존했습니다.
  • tenant-table access를 RLS와 validator가 이미 있는 monolith 안에 유지했습니다.
  • 주로 live request-response semantic을 요구하는 상호작용에 Redis나 MQ 동작을 추가하지 않았습니다.
  • Graph는 여전히 일반 compiled StateGraph이므로 LangGraph portability를 유지했습니다.
  • Resumable token replay를 약속하지 않으면서 final result durability를 제공했습니다.

이후 PgBouncer checkpointer issue는 connection-management 구현을 바꿨지만, 아키텍처 결정 자체를 바꾸지는 않았습니다. 해당 후속 작업은 Chatbot Checkpointer Single Connection Behind PgBouncer에서 다룹니다.

잘된 점

  • 가장 강력한 runtime이 아니라 실제 요구사항에 집중했습니다.
  • SQL ownership을 backend에 유지해 Python에 tenant/authorization rule을 중복하지 않았습니다.
  • Graph와 transport boundary가 단순해서 이후 hardening을 점진적으로 적용할 수 있었습니다.
  • Resumable stream replay가 실제 product requirement가 되면 Redis나 다른 orchestrator를 추가할 여지를 남겼습니다.

아쉬웠던 점

  • 초기 결정은 production-grade connection management를 나중 hardening에 의존했습니다.
  • Redis resumability와 execution durability가 서로 다른 문제라는 점을 명확히 설명해야 했습니다.
  • 원본 조사는 정확한 외부 research link를 보존하지 않아, 이 문서는 평가한 모든 자료가 아니라 결정 결과를 문서화합니다.

다시 검토할 조건

다음 요구사항이 실제로 생기면 이 아키텍처를 다시 검토합니다.

조건의미
사용자가 진행 중인 token stream에 재연결해야 합니다resumable stream storage 또는 동등한 replay layer가 필요합니다
graph execution이 worker pod death 중에도 살아남아야 합니다execution ownership을 더 강한 durable runtime으로 옮겨야 합니다
write-tool execution이 restart를 넘어 exactly-once semantic을 요구합니다queueing/replay 전에 더 강한 idempotency와 execution record가 필요합니다
여러 worker가 같은 live run에 attach해야 합니다external orchestration 또는 shared stream ownership을 검토해야 합니다

그 전까지 detached SSE plus backend-owned persistence는 기록된 요구사항을 만족하는 가장 작은 아키텍처입니다.

교훈

  • "작업을 잃지 않는다"는 먼저 persistence 요구사항이지, 곧바로 resumable-stream 요구사항은 아닙니다.
  • Redis는 event를 replay할 수 있지만 execution owner를 자동으로 정해주지는 않습니다.
  • MQ는 이 stack의 batch/callback worker에는 적합하지만 live token UX에 자동으로 맞는 것은 아닙니다.
  • 깨끗한 graph boundary는 더 무거운 runtime 선택을 나중으로 미뤄도 product를 막다른 길에 넣지 않게 해줍니다.