본문으로 건너뛰기Hwanseok Sim

작성일

Gem3Park 개발기 6 — 0 하나 차이 호수의 오승인을 막기까지

호출자가 1006호라고 했는데 106호로 확정된 통화에서 시작해, STT N-best 보존·위험 쌍 탐지·Gemini 우선·호출자 확인으로 오승인을 막은 8월 15일 하루의 설계와, 배포 직후 발견한 우회 경로를 정리했다.

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

허용 목록이 있는데도 틀렸다

5편의 허용 목록은 "없는 호수"를 막는다. 8월에는 반대 경우가 나왔다. 호출자는 1006호라고 말했는데 시스템이 106호로 확정하고 후속 출입 흐름을 진행했다. 두 호수가 모두 실제로 있어서 허용 목록은 둘 다 통과시켰다. 같은 차량의 앞선 통화에서도 같은 축약이 있었다.

녹음과 로그를 맞춰 보니 모델이 아니라 릴레이 코드가 원인이었다. 이렇게 흘러갔다.

  1. Google STT는 106호와 1006호를 N-best 대안으로 함께 돌려줄 수 있다.
  2. 릴레이는 자체 점수로 한 대안만 고르고 나머지를 버렸다.
  3. 같은 confidence에서 첫 대안의 순위 가점이 네 자리 숫자의 길이 가점보다 커서, 짧은 106호가 뽑힐 수 있었다.
  4. 두 값이 모두 허용 목록에 있으니 검사를 통과했다.
  5. 유효한 텍스트가 나오면 타는 빠른 경로 때문에 Gemini 동·호수 검증은 호출되지 않았다.
호출자: 999동 1006호
  -> Google N-best: 106 / 1006 근접 충돌
  -> 순위 중심 선택기가 한 값만 선택
  -> 999동 106호도 허용 목록에 있어 정상값으로 통과
  -> Gemini 검증 생략
  -> 잘못된 세대로 후속 처리

(예시 값은 가짜다.) 두 호수가 음향적으로 비슷한 것은 STT의 한계다. 그래도 오승인이 난 것은 충돌하는 유효 대안을 버리고 허용 목록 일치만으로 맞다고 본 릴레이 로직 때문이었다.

위험한 턴을 허용 목록에서 스스로 찾는다

특정 호수를 하드코딩하지 않았다. 현장 허용 목록에서 0 하나를 넣거나 빼면 서로가 되는 쌍을 자동으로 찾는다. 이 단지 목록에는 그런 쌍이 15개 동에 75개 있었다(저장소 포트폴리오 문서 기준, 101↔1001부터 106↔1006까지 여섯 유형). 위험 턴 판정은 두 조건 중 하나다.

  • N-best 안에 서로 다른 유효 동·호수 후보가 둘 이상 있다.
  • 고른 세대와 같은 동에서 0 하나 차이인 허용 세대가 있다.

판정 결과는 타입으로 구분해 로그에 남기도록 했다.

src/asterisk/building-unit-ambiguity.ts
export type BuildingUnitRiskKind =
  | "none"
  | "nbest-valid-conflict"
  | "single-zero-peer"
  | "both";

긴 호수를 무조건 선호하거나 106을 1006으로 자동 교정하지는 않는다. 그렇게 하면 반대 방향의 오승인이 생긴다.

Gemini가 먼저, 안 맞으면 호출자에게

위험 턴에서는 Google이 고른 값을 상태에 바로 확정하지 않고 보류한다. 같은 턴의 WAV를 Gemini에 전사시킨다.

Gemini 결과처리
허용 값이고 Google과 일치안전 상태로 확정
허용 값이고 Google과 불일치호출자에게 확인하기 전까지 출입 금지
실패, timeout, 무음, 불완전, 비허용Google 값으로 되돌아가지 않고 동·호수를 다시 질문

검증기가 죽었을 때 예전 값으로 물러나는 것은 검증이 없던 때와 같다. 모르면 다시 묻는다. 부정 확인이 오면 방문 목적은 유지하고 동·호수만 비워 다시 받는다. 출입 직전에도 안전 상태, 완전한 동·호수, 허용 목록, 미해결 여부를 한 번 더 본다.

위험 턴의 Gemini 요청은 오디오 전사 최대 한 번으로 못 박았다. 이때는 방문 목적 텍스트 분류 같은 다른 Gemini 호출을 억제해서 지연이 늘지 않게 했다. 비위험 턴은 기존 빠른 경로를 그대로 쓴다.

단계적으로 켜기

런북에는 모드가 셋이다. off, shadow(위험을 관측만 하고 기존 라우팅 유지), active(Gemini 우선, 확인, 차단). 모드 값이 빠졌거나 잘못되면 active로 본다. 다른 기능은 설정이 없으면 꺼지게 했지만, 이 가드는 설정 실수로 꺼지면 다시 오승인이 난다.

개인정보도 설계에 넣었다. N-best 원문은 그 턴의 판정에만 쓰고 세션 요약에는 남기지 않는다. 새 관측 기록은 최대 20건이고 원문 없이 메타데이터만 쓴다.

배포한 날 발견한 구멍

8월 15일에 진행 중인 통화가 없는 것을 두 번 확인한 뒤 PM2 릴레이와 모니터만 재시작해서 배포했다. Asterisk는 건드리지 않았다. 그날 독립 검토에서 테스트가 놓친 입력이 재현됐다. 숫자 뒤에 호출입처럼 호와 출이 붙어 나오는 붙여 쓴 STT 결과(예: 101호출입 때문에 관리사무소 방문)다. 7월 30일에 단독 119를 긴급으로 인정하면서 넣은 119 호출 예외가 너무 넓어서, 숫자 뒤에 호출이 이어지는 모든 입력이 대상 세대가 없는 발화로 잘못 판정됐다. 전체 흐름 재현에서는 출입 명령이 생성됐다.

그래서 이 패치를 완료로 치지 않았다. 고칠 기준은 정해 두었다. 101호출입은 대상 세대로 보고 다시 묻고, 119 호출은 대상 없는 응급 호출로, 119호 호출은 119호 세대로 본다. 이 경로는 지금도 완전히 닫히지 않았다(8편).

다음 글

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

이전 글: Gem3Park 개발기 5 — 허용 목록, 그리고 오디오 보조 설계를 줄인 이야기