Code › tail-villain
인터뷰 세션을 끝내는 조건
LLM의 완료 신호를 그대로 믿지 않고 인터뷰 종료 조건을 다시 세운 기록
인터뷰 세션이 사용자가 끝내기도 전에 completed 상태가 되어 있었다.
처음에는 UI 쪽에서 상태 표시를 잘못 읽은 줄 알았다. 세션 화면과 히스토리 모달을 계속 손보고 있었고, roadmap interview 화면도 study mode와 맞추는 중이었다. 카드 밀도, 스크롤 영역, 액션 메뉴 위치 같은 것들이 한꺼번에 바뀌고 있었기 때문에, completed 표시도 그 연장선에 있는 프론트 문제처럼 보였다.
그런데 실제로는 백엔드에서 세션을 끝내고 있었다. 사용자가 완료 버튼을 누른 것이 아니라, LLM 응답에 들어 있던 shouldComplete 값을 sendMessage 쪽에서 그대로 믿고 있었다.
tail-villain에서 인터뷰는 단순 채팅이 아니다. 사용자는 하나의 답변을 보내고, 빌런은 그 답변의 빈틈을 잡아 다음 질문을 던진다. 중간에 checkpoint가 있고, 각 angle이 열리며, 마지막에는 점수와 피드백이 붙는다. 이 흐름에서 세션 종료는 꽤 무거운 상태 전환이다.
LLM이 이제 끝내도 된다고 말하는 것과 제품이 정말 세션을 끝내는 것은 다른 일이다.
LLM은 현재 대화 맥락 안에서 판단하기 때문에, 답변이 충분해 보이면 shouldComplete를 true로 줄 수 있고 대화가 마무리되는 분위기라고 판단할 수도 있지만, 제품은 현재 angle이 마지막으로 공개된 checkpoint인지, 이전 angle들이 끝났는지, 평가가 충분히 강한지, 사용자가 직접 완료를 선택했는지까지 함께 확인해야 했다.
그래서 shouldComplete는 명령이 아니라 신호로 낮췄고, LLM이 true를 반환하더라도 final checkpoint gating을 통과할 때만 자동 완료를 허용하면서 수동 완료와 자동 완료의 조건을 분리했다.
같은 날 study mode 쪽에서도 비슷한 종류의 문제가 있었다.
study session을 시작할 때 500 에러가 났는데, 라우트나 API 경로가 아니라 저장되는 JSON payload가 문제였다. LLM이 만든 state나 report 안에 optional field가 빠지면서 undefined 값이 섞였고, 그 값이 Prisma JSON 필드에 그대로 들어가려 했다.
LLM 출력은 사람이 읽기에는 그럴듯해도, 저장 계층에서는 훨씬 엄격하게 다뤄야 한다. undefined가 섞인 객체를 그대로 저장하면 런타임에서는 작은 누락처럼 보여도, 데이터베이스 경계에서는 실패가 된다. 그래서 study state와 report payload를 Prisma-safe JSON으로 정규화한 뒤 저장하도록 고쳤고, optional field가 빠져도 persistence가 깨지지 않는 regression test를 추가했다.
여기서도 결론은 같았다. LLM이 만든 구조를 그대로 믿기보다, 저장하기 전에 제품의 데이터 규칙에 맞춰 정리해야 한다.
report 언어도 같은 범주였다.
한국어 roadmap에서 study completion report가 영어로 나오는 경우가 있었다. 대화 중 프롬프트에는 한국어 흐름이 있었지만, completion report 프롬프트에는 출력 언어를 명시하는 지시가 충분하지 않았다. LLM은 앞뒤 맥락을 어느 정도 따라오지만, 리포트처럼 별도의 산출물을 만들 때는 언어 요구사항을 다시 적어줘야 했다.
그래서 roadmap의 resolvedLanguage를 기준으로 completion report 언어를 명시했다. readiness나 level enum도 mock_ready, advanced 같은 내부 값이 그대로 보이지 않도록 localized label로 바꿨다. 사용자가 보는 리포트는 내부 상태 덤프가 아니라, 지금 어떤 수준이고 다음에 무엇을 해야 하는지 읽을 수 있는 결과물이어야 했다.
이 작업은 화려한 기능 추가가 아니었다. 하지만 이런 부분이 어긋나면 제품은 갑자기 실험 도구처럼 보인다. AI가 만든 텍스트, 데이터베이스에 저장되는 JSON, 사용자에게 보이는 label이 같은 방향을 봐야 했다.
프론트도 그 기준에 맞춰 정리했다.
roadmap interview session 화면은 이전보다 study session에 가까운 구조로 바꿨다. 헤더, 왼쪽 정보 rail, sticky chat panel, compact composer, assistant와 user message의 시각 언어를 다시 맞췄다. 다만 interview에는 checkpoint 진행 상태, completion state, score와 feedback 같은 차이가 있으므로 study mode와 완전히 같게 만들지는 않았다.
히스토리 모달도 같이 줄였다. 과거 세션 카드는 풍부한 dashboard card처럼 보이기보다, 빠르게 훑을 수 있는 목록이어야 했다. 카드 높이, 제목 계층, metric 표시를 낮추고, 스크롤 영역에서 위쪽이 잘리는 문제도 고쳤다. full analysis modal에서는 긴 chip이 scrollbar 아래로 잘리지 않도록 wrapping을 조정했다.
세션 삭제 액션도 같은 맥락이었다. 처음에는 roadmap interview hub에만 세 점 메뉴와 삭제 confirmation을 붙였는데, 실제 screenshot이 가리킨 곳은 roadmap detail history modal이었다. 같은 기능이라도 사용자가 보고 있는 화면이 다르면 구현 위치가 달라진다. 결국 hub와 detail history 양쪽에 delete flow를 맞추고, study session도 mixed session list에서 지울 수 있도록 백엔드까지 연결했다.
이날 한 일은 표면적으로는 여러 개였다. 메뉴 위치, study mode 500, report 언어, CTA 문구, interview session UI, history modal, delete action, auto-completion guard까지 서로 다른 작업처럼 보인다.
하지만 한 줄로 묶으면 세션 상태의 권한을 정리한 날이었다.
LLM은 답변을 만들고, 평가 신호를 주고, 다음 행동을 제안할 수 있다. 하지만 세션을 저장할 수 있는 형태로 바꾸는 일, 어떤 언어로 사용자에게 보여줄지 결정하는 일, 인터뷰를 정말 끝낼지 판단하는 일은 제품의 책임이다. 특히 completed 같은 상태는 한 번 바뀌면 사용자의 흐름을 끊어버릴 수 있으므로 더 보수적으로 다뤄야 한다.
AI가 끝났다고 말해도, 제품은 아직 끝낼 조건이 맞는지 확인해야 한다.
그게 이날 고친 가장 중요한 부분이었다.