Code › tail-villain
꼬빌이 따라야 할 인터뷰 상태
꼬빌의 말투와 질문 진행, 응답 언어를 제품이 관리하는 인터뷰 상태로 통합한 과정
꼬빌의 질문은 틀리지는 않았는데, 말투가 계속 미끄러지고 있었다.
사용자가 답변을 하면 꼬빌은 먼저 답변을 인정하고, 그다음에 압박 질문을 붙였다. 내용만 보면 후속 질문이 맞았지만 꼬빌이라는 persona에는 맞지 않았고, 차갑게 빈틈을 찌르는 인터뷰어가 아니라 예의 바른 챗봇이 잠깐 비판 모드로 바뀐 것처럼 들렸다.
처음에는 하지 말아야 할 문장을 프롬프트에서 더 강하게 금지했고, 몇 가지 표현을 직접 목록에 넣어 보기도 했다. 그런데 LLM은 금지어 목록을 읽으면서 오히려 그 패턴을 더 의식했는데, 말로는 금지하면서 예시로는 계속 그 문장을 보여 준 셈이었다.
더 결정적인 원인은 JSON Schema 쪽에 있었다. 응답 필드의 description이 acknowledgment나 transition sentence를 만들라고 설명하고 있었고, Gemini는 그 설명을 필드의 정의처럼 받아들였다. 시스템 프롬프트에서 아무리 차갑게 열라고 말해도, 구조화 출력의 필드 설명이 답변 인정문을 요구하고 있으면 모델은 그쪽을 따른다.
그래서 방향을 바꿔, 답변을 인정하지 말라고 반복하는 대신 reply 필드를 다음에 말할 challenge나 question으로 다시 정의했다. 평가 내용은 내부 assessment에만 남기고 사용자가 듣는 reply에는 새지 않게 했으며, 꼬빌의 예시도 답변을 요약한 뒤 반박하는 문장에서 바로 실패 모드나 근거 부족을 묻는 문장으로 바꿨다.
출력 필드의 정의를 바꾸자 실제 대화도 달라졌다. 꼬빌은 더 이상 “말씀하신 내용은 이해했습니다” 같은 문장으로 숨을 고르지 않고 곧바로 왜 그 판단이 버티는지 묻기 시작했다. persona는 성격 설명만으로 유지되지 않으며, 모델이 매번 채워 넣는 출력 필드 자체가 persona의 행동을 강제해야 한다.
그 다음에 드러난 문제는 말투가 아니라 인터뷰 상태였다.
라이브 테스트에서 왼쪽 패널은 이미 두 개의 angle이 끝났다고 표시하고 있었는데, 꼬빌은 여전히 Kafka와 Redis의 실패 모드를 묻고 있었다. 사용자는 다음 단계로 넘어간 줄 아는데, 인터뷰어는 이전 angle에 남아 있는 상태였다.
state machine 안의 한 줄이 문제를 만들고 있었다. 다음 angle index를 계산한 뒤 currentAngleIndex를 먼저 바꾸고 바로 그 값과 nextIndex를 비교했는데, 바꾼 뒤 비교하면 결국 같은 값을 비교하게 된다. 그 때문에 다음 angle의 state가 current로 바뀌지 않았고, progress builder는 현재 angle을 찾지 못했다.
그 결과 프롬프트에는 계속 첫 번째 angle이 현재 angle처럼 들어갔다. DB의 currentAngleIndex는 앞으로 가고, follow-up count도 다른 angle에 쌓이는데, LLM이 보는 checkpoint context는 첫 번째 angle에 묶여 있었다. 상태 하나가 틀어지자 UI, 프롬프트, 대화가 서로 다른 인터뷰를 보고 있었다.
일단 이전 index를 mutation 전에 저장해서 다음 angle을 current로 세팅하도록 고쳤다. 하지만 그걸로 끝나지 않았다. 더 깊은 문제는 LLM이 스스로 진행 여부를 판단하고, 동시에 다음 질문까지 만들어야 하는 구조였다.
모델이 moveToNextQuestion은 true로 두면서 nextQuestion을 비워 두면 상태만 앞으로 가고 화면에 전환 질문은 나오지 않았다. 반대로 cap이 찼는데도 대화 맥락이 강하면 모델은 계속 이전 주제를 파고들었다. 프롬프트로 “이제 넘어가라”고 밀어도, 긴 transcript 안에서 이미 달리고 있는 주제의 관성은 생각보다 강했다.
그래서 진행 권한을 백엔드로 가져왔다. follow-up turn은 현재 angle에 대한 평가와 질문만 만들고, cap에 도달하면 백엔드가 advance 여부를 계산한다. 새 angle opening은 별도 호출로 만들되 이전 transcript를 통째로 넣지 않았고, 새 angle 제목과 이전 angle의 짧은 요약만 넣어 모델이 다시 옛 주제로 끌려갈 재료를 줄였다.
완료도 같은 방식으로 정리했다. 마지막 angle에서 cap에 닿으면 열린 후속 질문을 남긴 채 completed가 되는 것이 아니라 별도의 closing 문장으로 세션을 끝낸다. 이 구조에서는 사이드바가 앞으로 가려면 실제 전환 문장이 먼저 렌더링되어야 하므로, 상태가 바뀌었는데 대화는 그대로인 경우를 제품 구조로 막을 수 있었다.
마지막으로 언어 기준도 같은 범주였다.
어떤 세션에서는 꼬빌이 영어로 시작했다가 첫 follow-up에서 한국어로 바뀌었다. 언어를 판단하는 기준이 경로마다 달랐기 때문이다. opening turn은 title의 dominant script를 보고 있었고, sendMessage는 roadmap의 resolvedLanguage를 보고 있었다.
title은 언어 판단 기준으로 약했다. 한국어 내용의 roadmap이라도 topic title에 영어가 들어갈 수 있고 반대도 가능하기 때문이다. 사용자의 preferredLanguage는 UI 표시 언어이므로 인터뷰 코칭 언어의 기준으로 사용할 수 없었다.
그래서 기준을 나눴다. 사용자의 preferredLanguage는 UI 언어로 두고, roadmap이나 topic 기반 세션의 코칭 언어는 roadmap.resolvedLanguage를 따른다. coding round처럼 roadmap content가 없는 경우에만 user preferredLanguage를 사용하며, title script detection은 제거했다.
꼬빌이 어떤 언어로 말해야 하는지도 인터뷰 상태의 일부다. 같은 세션 안에서 언어가 흔들리면 persona가 유지되더라도 사용자는 흐름을 잃는다.
돌아보면 이 며칠은 꼬빌을 더 똑똑한 AI로 만드는 작업이라기보다, 꼬빌이 따라야 할 상태를 제품이 더 분명하게 만든 시간이었다.
LLM은 답변을 만들 수 있고, 빈틈을 찾을 수 있고, 다음 질문을 제안할 수 있다. 하지만 어떤 angle에 있는지, 언제 넘어가야 하는지, 어떤 언어로 말해야 하는지, 평가 문장이 사용자에게 들려도 되는지는 제품이 정해야 한다. 그 경계가 흐리면 모델은 그럴듯한 말을 하면서도 인터뷰 흐름을 어긋낸다.
꼬빌의 말투를 유지하려면 현재 angle과 언어, 전환 조건, 구조화 출력 필드가 같은 인터뷰 상태를 가리켜야 했다.