[feat] LLM 채팅 스트리밍 API 및 호출 기록
name: Feature Request
labels: 'Type : Feature'
기능 요약
LLM 채팅 스트리밍 API(SSE)와 LLM 호출 기록을 추가한다.
상세 설명
채팅 API
- 메시지 전송
POST /api/chat→ SSE 스트리밍- 요청:
{conversationId, message} - 프론트는 이 엔드포인트 하나만 호출하고, 단계(stage)는 보내지 않는다 — 라우팅은 서버가 정한다
- 대화를 소유하지 않으면 스트림을 열기 전에 404
- 요청:
- 이벤트 4종을 순서대로 흘린다 (
route → token* → done, 오류 시error)이벤트 내용 횟수 route{stage, confidence, fallback}1회. 분류기가 아직 없어 항상 fallback token{text}토큰마다 done{messageId, seq, usage}정상 종료 시 1회 error{traceId, code, detail}오류 시 -
오류도 HTTP 200 안에서
error이벤트로 내려간다. 스트림이 열린 뒤에는 상태 코드를 바꿀 수 없다 -
EventSource가 아니라fetch()+ReadableStream을 전제로 한다 — POST여야 하고,EventSource의 자동 재연결이 생성을 다시 트리거하면 안 되기 때문
스레드·트랜잭션 경계
- 소유 검증과 사용자 메시지 저장은 스트림을 열기 전에 요청 스레드에서 끝낸다 (스트림이 열린 뒤 실패하면 오류가 SSE 안에 묻힌다)
- JPA는 블로킹이므로 DB 작업은
Schedulers.boundedElastic()으로 분리한다 -
SecurityContext는 스레드를 따라가지 않으므로 주체를 값으로 넘긴다 (AclContextOverride.callAs). 빠뜨리면 RLS의msg_insert정책이 전부 막는다 - 어시스턴트 응답은 스트림이 끝난 뒤
done단계에서 메시지로 저장한다
LLM 호출 기록 (observability)
- 모든 LLM 호출을
llm_call_logs에 1행 남긴다 — 단순 로그가 아니라 라우팅 정확도 평가용 데이터셋 - 컬럼: traceId, user, purpose, model, almStage, tokensIn/Out, latencyMs, status
-
CallPurpose: CLASSIFY_L1 / GENERATE / EMBED -
CallStatus: OK / ERROR / TIMEOUT -
REQUIRES_NEW트랜잭션이다 — 호출이 실패해 바깥이 롤백돼도 실패 기록은 남아야 한다 - 모델명은 설정값이 아니라 응답 메타데이터의 실제 모델명을 쓴다 (없을 때만 설정값)
- 비용(costUsd)은 단가 테이블이 없어 비워둔다
설정 (application.yml)
-
slexn.chat.memory-messages— 프롬프트에 실을 최근 메시지 수 (기본 20) -
slexn.chat.timeout— LLM 응답 지연 허용 (기본 60s, 초과 시error이벤트) -
slexn.chat.model— 로그에 남길 모델명 대체값 -
slexn.chat.system-prompt— ALM 맥락 지시