본문으로 건너뛰기Hwanseok Sim

작성일

Gem3Park 개발기 8 — timeout, 자기 정정 복구, 그리고 남은 과제

방문 목적 분류 호출의 48%가 timeout으로 버려지던 원인, 외부 결정 모델 Jev를 shadow로만 붙인 이유, 말을 고쳐 말한 동·호수를 복구하되 오복구 0을 우선한 결정, 그리고 아직 풀지 못한 과제를 정리한 마지막 편.

Gem3Park 개발기 시리즈 8/8편. 전체 목록은 gem3park 태그에서 볼 수 있다.

분류 호출의 절반이 버려지고 있었다

9월 28일 실측에서 방문 목적 분류 호출 77회 중 37회(48%)가 2.0초 timeout으로 버려졌다. 모델이 느린 줄 알았는데 원인은 달랐다. thinking 토큰이 작은 출력 한도(maxOutputTokens 32)를 먼저 다 써 버려서 응답이 비거나 잘렸다. 9월 29일에 thinking 토큰용 여유분 GEMINI_THINKING_TOKEN_HEADROOM(512)을 따로 두었다.

그 다음에는 모델을 바꿀지를 숫자로 정했다. 9월 실제 발화 300건, 호출 545회로 두 모델을 비교한 표다(저장소 포트폴리오 문서 기준).

항목3.7-flash (+여유분 512)3.6-flash
호출 지연 p50 / p90 / p992,043 / 3,295 / 8,345ms1,274 / 1,646 / 3,023ms
2.0초 초과 호출56.9%5.7%
라벨 획득 (시간 제한 없음)178건173건
기준값 일치 (시간 제한 없음)133건 (라벨 대비 74.7%)133건 (라벨 대비 76.9%)
라벨 획득 (운영 timeout 적용)140건173건
기준값 일치 (운영 timeout 적용)112건133건

시간 제한이 없으면 일치 건수는 같다. 그런데 운영 timeout을 적용하면 3.7은 라벨을 낸 호출이 38건 줄고 일치도 21건을 잃는다. 정확도가 같아도 시간 안에 오지 않으면 운영에서는 틀린 것과 같다. 여기서 기준값은 통화가 최종 전송한 방문 목적이고, 사람이 검수한 정답은 아니다. 이 시리즈의 일치율은 모두 그 기준이다. 그래서 방문 목적 텍스트 호출만 3.6으로 옮겼다. 대화 LLM의 기본은 3.7로 두고, HTTP 503일 때만 3.6으로 한 번 폴백한다.

src/gemini-model-config.ts
export const GEMINI_GENERATE_CONTENT_MODEL = "gemini-3.7-flash";
export const GEMINI_FALLBACK_MODEL = "gemini-3.6-flash";

외부 결정 모델은 shadow로만

같은 날(9월 29일) Jev라는 외부 결정 모델을 방문 목적 판정 앞에 붙였다. Jev는 문장을 만들지 않는다. 텍스트를 받아 정해진 선택지별 확률만 돌려준다. 한 번의 요청에 좁은 질문 열한 개를 묻고, 모든 임계값을 넘을 때만 라벨을 받아들인다. 아니면 기존 Gemini 체인이 그대로 돈다. 차량번호는 보내기 전에 가린다. 기본 모드는 off다.

공개된 속도·정확도는 공급사 주장이었고 한국어 근거는 아예 없었다. 그래서 통화 결과에 영향이 없는 shadow로 시작했고, 기존 통화 발화로 offline replay를 돌렸다. 키워드로 풀리지 않은 9월 발화 442건 중 67건(15.2%)을 확정했고 그중 63건(94.0%)이 통화의 최종 목적과 같았다. 지연 p50은 205ms였다. 그래서 active 전환은 보류했다. 운영 shadow 60건에서는 34건이 양쪽 모두 라벨이 없었고 일치는 2건뿐이었다. 표본이 이렇게 적으니 임계값 하나(minStated)를 0.6에서 0.53으로 낮춘 것 외에는 건드리지 않았다.

말을 고쳐 말한 동·호수

9월 28일부터 10월 1일까지 426통화 중 방문증 발급 완료가 345건, 상담원 전환이 20건이었다. 발급 345건을 발급과 전환을 합한 365건으로 나누면 94.5%, 전체 426건으로 나누면 81.0%다. 제외한 61건에는 소켓 연결 종료 56건 등이 들어 있다. 전환 20건 중 16건이 "동호수 재질문 횟수 초과"였다.

그중 한 통화를 오디오로 다시 들었다. Gemini 보조 전사는 정정까지 맞게 들었다(가짜 예: 999동 13 9999호요, 앞의 13은 말을 고치기 전의 조각). 그런데 파서가 첫 매치만 써서 13을 호수로 읽었고 허용 목록에서 거절됐다. 첫 발화가 목적뿐이어서 동·호수 질문은 이미 1/2였고, 이 미스로 2/2가 되어 바로 전환됐다. 동·호수를 말할 기회가 한 번뿐이었다.

고친 것은 두 가지다. 하나는 첫 후보가 허용 목록에 없을 때만 같은 발화의 다른 후보를 보되, 허용 후보가 정확히 하나일 때만 채택하는 추출기다. 다른 하나는 허용 목록 미스가 난 턴의 재질문을 한도에서 빼 주는 GUIDED_ALLOWLIST_MISS_GRACE_TURNS(기본 1)다.

잘못 복구하면 엉뚱한 세대로 출입 명령이 나간다. 그래서 복구하지 않는 경우부터 정했다.

사유조건
split-unit-number인접한 숫자를 붙이면 허용 호수가 되는 경우
conflicting-unit-mentions호가 붙은 숫자가 둘 이상 다른 경우
single-zero-peer복구 후보와 0 하나 차이인 호수가 목록에 있는 경우

저장된 발화 11,738건(7월 30일 이후, 미스 발화 460건)에 새 파서를 재생해 비교했다.

복구최종 발급과 일치불일치확인 불가
금지 규칙 없이16844
금지 규칙 적용 (배포본)9801

금지 규칙이 없으면 16건을 살리지만 잘못된 복구가 4건 생긴다. 규칙을 넣으면 9건을 살리고 잘못된 복구는 없다. 둘 다 일치는 8건이라 살린 양은 비슷한데 틀린 것만 사라졌다. 규칙을 넣은 판을 배포했다. 면제 턴은 통화 흐름 전체의 일이라 재생으로 잴 수 없어서 단위·브리지 테스트로만 검증했다(npm test 736개 → 771개).

중고거래 방문 정책

10월 14일에는 중고거래 방문 정책을 넣었다. 허용 목록 동·호수가 있는 호출자만 들이고, 없으면 재질문 예산과 별도로 한 번 묻고, 그래도 없으면 돌아가 달라는 안내를 하고 통화를 끝낸다. 출입 명령도 상담원 전환도 보내지 않는다. SECONDHAND_TRADE_POLICY=off로 이전 동작으로 돌릴 수 있고 결과는 통화 요약에 남는다.

아직 풀지 못한 것

  • 동은 맞는데 그 동에 없는 호수로 들은 경우. 전환 59건으로 가장 많은데 후보가 여럿이라 이번 방식으로는 풀리지 않는다. 전사문 조사부터 필요하다.
  • 붙여 쓴 STT 결과가 가드를 우회하는 경로(6편)가 남아 있다.
  • 근접 동 확인 질문("이 동이 맞으신가요?")은 설계만 했다. 후보 정확도가 66.7%였고 동은 맞아도 호수가 틀린 후보를 물을 수 있어서 shadow로 적중률부터 재기로 했다.
  • 사람이 확인한 정답 데이터. 모니터링 웹의 검수 기록이 쌓여야 정확도를 말할 수 있다.
  • Jev active 전환, Gemini Live의 offline 300턴·shadow 1,000창 합격 단계는 아직 기록이 없다.
  • Google STT 모델. 10월 16일 검토 결론은 지금 쓰는 telephony_short를 유지하되, 최신 세대인 V2 chirp_3를 실패 WAV와 정상 통화 표본으로 shadow 비교해 보자는 것이다.

이전 글: Gem3Park 개발기 7 — 두 번째 귀와 모니터링 웹