본문으로 건너뛰기

챗봇

목적

chatbot-svc는 Lumie academy assistant를 뒷받침하는 LangGraph 워커입니다. 백엔드는 채팅 요청을 이 워커로 프록시하고, 워커는 Server-Sent Events(SSE)로 assistant 출력을 스트리밍하며, 모든 테넌트 데이터 접근은 백엔드 /internal/chatbot/** 엔드포인트로 다시 위임됩니다.

이 페이지는 worker, backend chat proxy, tool contract, LangGraph persistence, chatbot 운영을 변경하는 개발자를 위한 reference 문서입니다. 섹션 전반의 worker 규칙은 Workers Overview를 참조하세요.

소스 경로

경로역할
lumie-worker/services/chatbot/main.pyFastAPI 앱, lifespan setup, /health, /metrics, /api/chatbot/stream, /api/chatbot/confirm
lumie-worker/services/chatbot/src/schema.pystream 및 confirm 호출용 Pydantic 요청/응답 envelope
lumie-worker/services/chatbot/src/usecase.pySSE 스트리밍, 분리된 graph run, pending-action 영속화, confirmation 재개, 오류 매핑
lumie-worker/services/chatbot/src/graph/agent.pyLangGraph topology, scope guard, LLM 노드, tool 노드, retry 라우팅, cancellation 라우팅
lumie-worker/services/chatbot/src/graph/tools.py모델에 전달하는 도구 사양과 사람이 읽을 수 있는 액션 설명
lumie-worker/services/chatbot/src/adapters/backend_api.py백엔드 /internal/chatbot/** 엔드포인트용 HMAC 서명 HTTP adapter
lumie-worker/services/chatbot/src/config.pyLLM, backend, LangGraph DB, observability 관련 환경 설정
lumie-backend/modules/ai/src/main/java/com/lumie/ai/adapter/out/external/ChatbotClient.java워커를 호출하고 SSE frame을 프록시하는 백엔드 클라이언트
lumie-backend/modules/ai/src/main/java/com/lumie/ai/adapter/in/web/InternalChatbotController.javaSQL, 도구, 스키마, 영속화를 위해 워커가 사용하는 백엔드 콜백 표면
lumie-infra/applications/lumie/worker/chatbot-svc/common-values.yaml운영 배포, 서비스, env, secret, ServiceMonitor 연결

공개 표면

chatbot-svc는 내부 ClusterIP 서비스입니다. 브라우저가 직접 호출하지 않으며, 프론트엔드 채팅 요청은 백엔드 /v1/chat/**를 거친 뒤 백엔드 ChatbotClient가 워커를 호출합니다.

엔드포인트목적
GET /healthprobe 엔드포인트, lifespan container 없이 {"status":"ok"} 반환
GET /metricsmetrics_app에서 마운트한 Prometheus 엔드포인트
POST /api/chatbot/stream채팅 턴을 시작하거나 이어가며 SSE 스트림 반환
POST /api/chatbot/confirm사용자 승인 또는 거절 후 중단된 write tool 액션 재개

두 POST 라우트 모두 X-Tenant-Slug를 요구합니다. main.py::_require_tenant_match(...)는 헤더가 본문 tenantSlug와 일치하지 않으면 HTTP 400으로 거부합니다. lifespan container 초기화가 끝나지 않았다면 두 POST 라우트는 모두 HTTP 503을 반환합니다.

런타임 플로우

ChatUseCase.stream_chat(...)는 graph를 분리된 백그라운드 task에서 시작합니다. HTTP 응답은 제한된 크기의 SSE frame 큐만 비웁니다. 클라이언트가 연결을 끊어도 graph task는 계속 실행되며 최종 assistant 메시지를 백엔드를 통해 영속화합니다.

스트림 계약

POST /api/chatbot/streamChatStreamRequest를 받습니다.

class ChatStreamRequest(BaseModel):
tenant_slug: str = Field(..., alias="tenantSlug", min_length=1)
tenant_id: int = Field(..., alias="tenantId")
user_id: int = Field(..., alias="userId")
conversation_id: int | None = Field(None, alias="conversationId")
message: str = Field(..., min_length=1, max_length=4000)

conversationId가 null이면 워커는 백엔드에 conversation 생성을 요청하고, 마지막 done 이벤트에서 새 id를 반환합니다.

예시 요청 본문:

{
"tenantSlug": "demo",
"tenantId": 7,
"userId": 101,
"conversationId": null,
"message": "이번 주 공지사항을 만들어 줘"
}

스트림은 다음 SSE 이벤트를 내보냅니다.

EventData shape의미
token{"content": "..."}assistant 텍스트 청크. tool 메시지와 원시 SQL 결과는 필터링됩니다.
pending{"messageId": 1, "toolName": "...", "description": "...", "arguments": {...}}write tool이 실행 전에 사용자 확인을 필요로 함
done{"conversationId": 1, "messageId": 2}턴이 끝났거나 pending action이 영속화됨
error{"message": "..."}시작 이후 분리된 graph run이 실패함

백엔드 ChatbotClient는 worker 스트림을 브라우저용 SseEmitter로 프록시하면서 event 이름과 data를 그대로 보존합니다.

현재 worker 계약의 짧은 transcript:

event: token
data: {"content":"안녕"}

event: token
data: {"content":"하세요"}

event: done
data: {"conversationId":10,"messageId":52}

이 transcript는 ChatUseCase._sse(...) framing 및 services/chatbot/tests/test_usecase.pytest_stream_emits_tokens_and_persists_both_messages 범위와 일치합니다.

확인 계약

POST /api/chatbot/confirmConfirmRequest를 받아 ChatReplyResponse를 반환합니다.

class ConfirmRequest(BaseModel):
tenant_slug: str = Field(..., alias="tenantSlug", min_length=1)
tenant_id: int = Field(..., alias="tenantId")
user_id: int = Field(..., alias="userId")
conversation_id: int = Field(..., alias="conversationId")
message_id: int = Field(..., alias="messageId")
confirmed: bool

예시 confirm 요청:

{
"tenantSlug": "demo",
"tenantId": 7,
"userId": 101,
"conversationId": 10,
"messageId": 52,
"confirmed": true
}

백엔드 공개 /v1/chat/confirm 라우트는 messageIdconfirmed만 받으며, 워커를 호출하기 전에 pending message에서 conversationId를 해석합니다. 백엔드 idempotency는 워커 내부가 아니라 /v1/chat/confirm의 선택적 Idempotency-Key 헤더에서 처리됩니다.

확인 후 graph가 재개되었을 때 또 다른 write action이 요청되면 응답에 새로운 pendingAction이 들어갑니다. 그렇지 않으면 최종 assistant 메시지와 pendingAction=null을 반환합니다.

pending action이 완료되었을 때의 예시 응답:

{
"conversationId": 10,
"message": "완료했습니다.",
"pendingAction": null
}

LangGraph가 또 다른 write tool에서 다시 멈추면 confirm_tool(...)은 최종 assistant 응답 대신 message: ""와 non-null pendingAction을 반환합니다.

백엔드 콜백 표면

워커는 tenant 데이터 연결을 직접 열지 않습니다. 대신 HMAC 서명된 JSON 본문과 X-Tenant-Slug를 사용해 백엔드 /internal/chatbot/** 엔드포인트를 호출하는 BackendApiClient를 사용합니다.

백엔드 엔드포인트worker 메서드책임
POST /internal/chatbot/queryexecute_query(...)GeneralQueryTool, SqlValidator, RLS, 읽기 전용 풀을 통해 read-only SQL 실행
POST /internal/chatbot/tools/{toolName}execute_tool(...)사용자 확인 후 allowlist에 있는 write tool 실행
GET /internal/chatbot/schemaget_schema(...)시스템 프롬프트용 tenant schema 설명 반환
GET /internal/chatbot/historyget_history(...)최근 대화 메시지 반환, 현재는 history hydration 용도로 예약
POST /internal/chatbot/pending-actionsave_pending_action(...)LangGraph interrupt를 pending action 메시지로 영속화
POST /internal/chatbot/save-messagesave_message(...)사용자, assistant, tool 메시지 영속화
POST /internal/chatbot/conversationscreate_conversation(...)새 conversation 생성 후 id 반환

InternalChatbotController는 SQL 또는 tool 실행 전에 tenant 및 user 컨텍스트를 다시 세우므로, RLS와 기존 백엔드 tool 구현이 계속 제어 지점으로 남습니다.

LangGraph 모델

src/graph/agent.py는 다음 graph를 컴파일합니다.

모델은 langchain_openai.ChatOpenAI로 구성되며, 기본값으로 OpenAI 호환 엔드포인트를 통한 Gemini를 사용합니다.

설정기본값
LLM_BASE_URLhttps://generativelanguage.googleapis.com/v1beta/openai
LLM_MODELgemini-2.5-flash
LLM_API_KEYrequired

프로세스 프롬프트는 services/chatbot/prompts/chat-system.txt에서 로드되어 프로세스 수명 동안 캐시됩니다. 프롬프트 수정에는 pod 재시작이 필요합니다.

도구 및 HITL 모델

graph는 모델에 하나의 read tool과 네 개의 write tool을 노출합니다.

Tool타입실행 경로
general_queryReadPOST /internal/chatbot/query를 통해 즉시 실행
create_announcementWriteinterrupt(...)로 일시 중지 후 확인 뒤 백엔드 tool로 실행
send_smsWriteinterrupt(...)로 일시 중지 후 확인 뒤 실행
send_telegramWriteinterrupt(...)로 일시 중지 후 확인 뒤 실행
schedule_taskWriteinterrupt(...)로 일시 중지 후 확인 뒤 실행

read 실패는 sql_retry_count를 증가시키고 chatbot_sql_retries_total을 기록하며, SQL_MAX_RETRIES에 도달할 때까지 다시 모델로 라우팅됩니다. write tool은 최초 stream 턴에서 실행되지 않습니다. 먼저 pending action을 저장하고 pending 이벤트를 내보낸 뒤 /api/chatbot/confirm을 기다립니다.

영속화와 시작

워커는 두 가지 영속화 경로를 사용합니다.

영속화 경로소유자목적
백엔드 ai_conversations 및 메시지백엔드 ChatServiceconversation id, 사용자 메시지, assistant 메시지, pending action
LangGraph checkpointer 테이블AsyncPostgresSaverthread_id = "{tenantId}:{conversationId}"용 graph 상태

main.py의 lifespan 시작 절차:

  1. 동기 adapter와 공유 httpx.AsyncClient를 구성합니다.
  2. LANGGRAPH_DB_DSN으로 AsyncConnectionPool을 엽니다.
  3. AsyncPostgresSaver(pool)를 만들고 setup()을 실행합니다.
  4. chat model을 구성합니다.
  5. live checkpointer와 함께 LangGraph를 컴파일합니다.
  6. graph를 ChatUseCase에 부착합니다.

Postgres 풀은 PgBouncer transaction-mode 호환성을 위해 autocommit=True, prepare_threshold=0, row_factory=dict_row, max_idle=180.0으로 설정됩니다.

설정 및 배포

src/config.pychatbot-svc/common-values.yaml가 런타임 계약을 정의합니다.

설정필수 여부비고
LUMIE_BACKEND_URLYes/internal/chatbot/** 콜백용 백엔드 base URL
LUMIE_INTERNAL_HMAC_SECRETYes서명된 내부 호출용 공유 secret
LLM_API_KEYYesVault에서 가져오는 Gemini API 키
LLM_BASE_URLNo기본값은 OpenAI 호환 Gemini 엔드포인트
LLM_MODELNo기본값은 gemini-2.5-flash
LANGGRAPH_DB_DSNYesCNPG pooler를 통한 전용 lumie_langgraph role용 DSN
OTEL_ENABLEDNo기본값 true, 로컬 테스트에서는 비활성화 가능
OTEL_ENDPOINTNo클러스터 collector용 OTLP gRPC 엔드포인트
RECURSION_LIMITNograph 최대 재귀 단계 수, 기본값 10
SQL_MAX_RETRIESNoread-query 복구 최대 시도 횟수, 기본값 3

운영 배포는 ingress가 비활성화된 ClusterIP 전용입니다. Prometheus는 ServiceMonitor를 통해 /metrics/를 스크레이프합니다.

실패 및 백프레셔 동작

실패 지점동작
X-Tenant-Slug 불일치use case 진입 전에 HTTP 400 반환
lifespan container 준비 안 됨stream과 confirm 라우트에 HTTP 503 반환
스트림 중 클라이언트 연결 종료분리된 graph task는 계속 실행되며 최종 assistant 메시지를 영속화
느린 SSE 소비자_emit(...)가 최대 5초 대기 후 live frame을 버리지만 graph run은 계속 유지
graph 재귀 한도 초과fallback assistant 메시지를 내보내고 영속화하며 chatbot_errors_total{error_type="recursion"} 기록
예기치 못한 graph 예외error SSE frame을 내보내고 chatbot_errors_total{error_type="exception"} 기록
백엔드 SQL 실패tool 노드가 SQL_MAX_RETRIES까지 모델을 통해 재시도한 뒤 give_up으로 라우팅
사용자가 write tool 거절graph가 결정적인 취소 텍스트로 라우팅되며 백엔드 tool은 실행되지 않음
모델이 알 수 없는 tool 요청tool 노드가 해당 tool이 알 수 없다는 tool 메시지를 기록하며 백엔드는 그 tool에 대해 호출되지 않음

종료 시에는 진행 중인 분리 graph task를 취소하고, 공유 HTTP client를 닫은 다음, 체크포인트 쓰기가 먼저 끝날 수 있도록 Postgres 풀을 마지막에 닫습니다.

관측성

metric은 src/observability/metrics.py에서 export됩니다.

MetricLabels의미
chatbot_stream_duration_secondsoutcomestream 실행 종단 간 소요 시간. outcome은 completed, interrupted, error
chatbot_sql_retries_totaltenantread-query 재시도 횟수
chatbot_hitl_totalconfirmed사람의 확인 결정 횟수
chatbot_errors_totalerror_typerecursion 또는 exception 유형의 graph 실패 수

Tracing은 FastAPI 요청용 OpenTelemetry와 LLM, tool, chain span용 OpenInference LangChain 계측을 사용합니다. 전역 httpx 계측은 이 서비스에서 ChatOpenAI 구성을 깨뜨리므로 의도적으로 사용하지 않습니다.

검증

worker 변경에는 chatbot 테스트 슬라이스를 사용하세요.

cd lumie-worker
pytest services/chatbot/tests

예상 성공 신호:

  • pytest0으로 종료
  • services/chatbot/tests/test_health.pyGET /health{"status":"ok"}를 반환함을 증명
  • 같은 테스트 파일이 lifespan 시작으로 container가 아직 부착되지 않았을 때 POST /api/chatbot/stream503을 반환함도 증명
  • services/chatbot/tests/test_usecase.pytoken, pending, done, disconnect durability, confirm-path persistence를 다룸

계약 변경 시에는 worker, backend proxy, backend internal callback surface를 함께 검사하세요.

rg -n "api/chatbot|internal/chatbot|ChatbotClient|InternalChatbotController" \
lumie-worker/services/chatbot lumie-backend/modules/ai

배포 변경 시에는 worker Helm 값과 backend service URL을 함께 검사하세요.

rg -n "CHATBOT_SVC_URL|chatbot-svc|LANGGRAPH_DB_DSN|LLM_MODEL" \
lumie-infra/applications/lumie lumie-backend/Tiltfile lumie-worker/Tiltfile