AI 협업 개발

Whisper 프롬프트는 앞에서부터 224 토큰으로 소리 없이 잘립니다.

Whisper의 `prompt` 매개변수에는 숨겨진 224 토큰 제한이 있습니다. 초과분은 경고 없이 앞에서부터 잘리며, 이는 키워드 편향을 버그로 만들 수 있습니다.

이 글은 영어 원문을 AI 모델이 번역한 것입니다. 표현이 원문과 다를 수 있습니다. 영어 원문 보기

저희는 Whisper 음성 인식을 위한 키워드 편향 프롬프트를 배포했는데, 그로 인해 음성 인식 결과가 더 나빠졌습니다. 단순히 조금 나빠진 것이 아니라, 프롬프트가 모델이 잘못된 브랜드 이름을 선택하도록 적극적으로 유도했습니다. 근본 원인은 API가 전혀 보고하지 않는 단위 불일치였습니다. Whisper의 프롬프트 매개변수에는 224 토큰이라는 실질적인 제한이 있으며, 이를 초과하는 모든 것은 조용히 삭제되고, 삭제되는 부분은 앞부분입니다. 이 게시물에서는 블랙박스 A/B 테스트를 통해 저희가 그 동작을 어떻게 증명했는지, 그리고 문제를 해결한 예산 산정 방식에 대해 자세히 설명합니다.

prompt 매개변수의 실제 역할

Whisper는 각 전사 요청 시 선택적 프롬프트를 허용합니다. 공식 가이드에 따르면 프롬프트는 "모델의 스타일을 유도하거나 익숙하지 않은 단어의 철자를 지정"하는 데 사용될 수 있습니다. 의료 상담, 법률 구술, 제품 지원 통화와 같이 특정 분야의 전문 용어가 많은 오디오의 경우, 호스팅된 API에서 사용할 수 있는 주요 수단은 프롬프트에 해당 분야의 어휘를 나열하는 것입니다. 그러면 모델이 해당 용어의 철자를 올바르게 표기할 가능성이 훨씬 높아집니다.

내부적으로 프롬프트는 모델이 추론하는 자유로운 텍스트가 아닙니다. 프롬프트는 인코딩되어 이전 오디오 세그먼트의 전사 내용인 것처럼 디코더의 컨텍스트 창에 배치됩니다. 모델은 그 지점부터 이어갑니다. 이러한 구조는 우리를 놀라게 했던 두 가지 속성을 설명합니다.

  • 프롬프트는 컨텍스트 공간을 두고 출력과 경쟁합니다. Whisper의 텍스트 컨텍스트는 448개 토큰이며, 구현 시 프롬프트를 위해 최대 절반에서 하나를 뺀 223~224개 토큰을 예약합니다.
  • 프롬프트가 너무 길면 참조 구현은 앞부분이 아닌 뒷부분을 유지합니다. 원본 소스에서 이는 prompt_tokens[-(n_ctx // 2 - 1):]라는 한 줄짜리 슬라이스입니다. 이 경계 이전의 모든 것은 모델이 보기 전에 사라집니다.

이 두 가지 속성 모두 호스팅된 API에서는 드러나지 않습니다. 저희가 사용한 제공업체는 896자라는 별도의 제한을 검증하고, 이를 초과하면 오류를 반환합니다. 토큰 제한에 대해서는 아무런 경고도 표시되지 않는데, 이는 잘라내기가 요청 검증 단계가 아닌 모델 런타임 내부에서 발생하기 때문입니다.

유용한 프롬프트가 해롭게 변하는 과정

저희 프롬프트는 용어집 데이터베이스로부터 조합되었습니다. 가장 중요한 브랜드 이름의 수기 시드 목록을 먼저 넣고, 바이트 예산이 찰 때까지 데이터베이스 용어를 추가하는 방식이었습니다. 예산은 850바이트였는데, 이는 공급자의 오류 메시지에 있는 "896" 제한에 맞춰 선택한 것이었고, 저희는 이 제한이 바이트 단위라고 가정했습니다.

세 가지 문제가 겹쳤습니다.

  1. 896이라는 제한은 바이트가 아니라 문자 기준입니다. 한국어 텍스트는 UTF-8에서 문자당 3바이트이므로, 850바이트 프롬프트는 약 280자에 불과합니다. 저희는 예산에 거의 근접했다고 생각했지만, 실제로는 문자 예산의 3분의 1도 채 사용하지 않고 있었습니다.
  2. 그 280자는 약 380개의 토큰이었습니다. CJK 스크립트는 Whisper의 BPE 어휘에서 토큰화 비용이 비싸며, 저희 측정치로는 문자당 대략 1.2에서 1.4개의 토큰이었습니다. 모든 단일 요청에서 224개 토큰 상한을 70% 초과했습니다.
  3. 초과분은 앞에서부터 잘렸는데, 그곳은 바로 저희가 가장 중요한 용어들을 배치한 곳이었습니다.

실패 모드는 악순환이었습니다. 저희 시드 목록은 사용자들이 가장 자주 말하는 브랜드 이름으로 시작했습니다. 이 이름들이 가장 먼저 사라졌습니다. 순전히 데이터베이스 행 순서라는 우연 때문에 살아남은 것은 프롬프트 끝부분에 있던, 비슷하게 들리지만 잘못된 용어들의 묶음이었습니다. 그래서 사용자가 "Sonalift"(이 게시물의 모든 브랜드 이름은 가명입니다) 같은 말을 하면, 살아남은 유사한 단어 "Sonaline"으로 사전 정보를 받은 모델은 확신을 갖고 잘못된 브랜드를 전사했습니다. 편향이 전혀 없는 프롬프트가 저희 것보다 더 나은 성능을 보였는데, 저희 프롬프트는 실수 쪽으로 편향을 유도하고 있었기 때문입니다.

주목할 만한 반전이 하나 더 있었습니다. 조합 쿼리에 ORDER BY 절이 없었습니다. 1,200개의 후보 용어 중 어떤 40개가 프롬프트에 들어갈지는 데이터베이스의 물리적 행 순서에 따라 결정되었는데, 이 순서는 vacuum 및 대량 쓰기 작업 후에 바뀝니다. 동일한 코드, 동일한 오디오가 날마다 다른 전사 결과를 생성했습니다. 만약 편향이 비결정적으로 작동한다면, 모델을 탓하기 전에 용어 선택이 실제로 결정적인지 확인해 보십시오.

위치 기반 A/B 테스트로 전방 절단 증명하기

절단 방향을 확인하기 위해 모델 내부에 접근할 필요는 없습니다. 한도를 초과하는 프롬프트 하나와 그 프롬프트의 두 가지 배열만 있으면 됩니다.

우리는 약 460자, 대략 400토큰의 프롬프트를 만들었습니다. 이는 224토큰 상한선을 여유롭게 넘으면서도 제공업체의 896자 유효성 검사 기준은 넘지 않는 길이입니다. 그리고 동일한 오디오 파일을 이 프롬프트를 사용해 두 번 실행했습니다.

  • 배열 A: 세 개의 핵심 용어를 앞에 배치하고, 그 뒤에 채우기용 어휘를 넣었습니다.
  • 배열 B: 동일한 채우기용 어휘를 먼저 넣고, 맨 끝에 같은 세 용어를 배치했습니다.

결과는 명확했습니다. 배열 A는 세 용어를 모두 잘못 전사하여 운영 환경의 버그를 정확히 재현했습니다. 배열 B는 세 용어를 모두 정확하게 전사했습니다. 내용, 토큰 수, 오디오는 동일했고 위치만 바뀌었습니다. 이것이 소스 접근 없이 외부에서 관찰한 전방 절단입니다.

여기서 실용적인 규칙이 바로 나옵니다. Whisper 프롬프트에서는 끝부분이 가장 중요한 자리입니다. 만약 절단이 발생하면, 마지막에 배치한 것이 무엇이든 살아남습니다. 반드시 포함해야 하는 용어는 꼬리 부분에 넣고, 머리 부분은 희생 구역으로 취급하십시오.

토크나이저 없이 토큰 예산 책정

영구적인 해결책은 애초에 한도를 초과하지 않는 것이며, 이는 전송 전에 토큰을 세는 것을 의미합니다. 우리는 이 한 가지 목적을 위해 Go 서비스에 BPE 토크나이저를 내장하고 싶지 않았기에, 프로덕션 어휘에 대해 실제 토크나이저와 비교하여 측정한 스크립트별 상한 계수를 사용하는 추정기를 만들었습니다.

func estimateTokens(s string) int {
    sum := 0.0
    for _, r := range s {
        switch {
        case r > 0xFFFF: // beyond BMP: surrogate pairs, emoji
            sum += 2.0
        case unicode.Is(unicode.Hangul, r) || unicode.Is(unicode.Han, r) ||
            unicode.Is(unicode.Hiragana, r) || unicode.Is(unicode.Katakana, r):
            sum += 1.4
        case unicode.Is(unicode.Thai, r):
            sum += 1.0
        case unicode.Is(unicode.Cyrillic, r) || unicode.Is(unicode.Arabic, r):
            sum += 0.55
        case (unicode.IsLetter(r) && unicode.Is(unicode.Latin, r)) ||
            unicode.IsDigit(r) || r == ' ':
            sum += 0.45
        default: // punctuation and symbols
            sum += 1.0
        }
    }
    return int(math.Ceil(sum))
}

정확한 숫자보다 세 가지 설계 결정이 더 중요합니다.

  • 전체 문자열을 분류하지 말고, 룬(rune)을 스캔합니다. 실제 어휘는 단일 용어 내에 여러 스크립트를 혼합합니다("V-line", "3D lift", 숫자가 포함된 브랜드 이름). 언어별 단일 계수는 여러분이 중요하게 생각하는 바로 그 용어를 잘못 추정합니다.
  • 계수는 평균이 아니라 상한값이어야 합니다. 15% 미달하는 평균 추정기는 여러분이 방지하려는 잘림(truncation)을 조용히 다시 발생시킵니다. 우리는 224 토큰 상한에 대해 내부 예산을 200 토큰으로 설정했으므로, 추정기는 초과할 수는 있지만 절대 미달해서는 안 됩니다.
  • 구두점은 "기타 라틴 문자"가 아닙니다. 저희 첫 초안에서는 CJK가 아닌 모든 것에 0.45를 사용했습니다. "A/B/C-123"과 같은 문자열과 상표 기호는 문자당 거의 1토큰으로 토큰화되므로, 기호들은 자체적인 1.0 레인을 갖게 되었습니다.

테스트 스위트는 실제 Whisper 토크나이저로 오프라인에서 측정한 토큰 수를 가진 고정 문자열(fixture string)을 고정하고, 모든 고정 문자열에 대해 estimate >= actual을 단언(assert)했습니다. 그 단언문은 배포 전에 실제 버그를 잡아냈습니다. 저희의 태국어 계수 0.7은 일반적인 사전 단어를 평균하여 나온 것이었지만, 실제로 편향 프롬프트(biasing prompt)를 채우는 음역된 브랜드 이름은 문자당 약 1.0 토큰으로 토큰화되었습니다. 상한 테스트가 실패했고, 저희는 계수를 높였으며, 태국어 사용자를 위한 소리 없는 잘림(silent truncation)의 한 부류 전체가 출시되지 않았습니다. 단언문은 엄격한 부등식으로 작성하십시오. "어느 쪽이든 15% 이내"라는 허용 오차는 그 버그를 통과시켰을 것입니다.

나머지를 채우기 전에 꼬리 예산 확보

토큰을 셀 수 있게 되면 조립 순서는 여전히 중요합니다. 두 가지 규칙이 최종 프롬프트를 견고하게 만들었습니다.

첫째, 다른 어떤 것을 허용하기 전에 핵심 용어에 대한 예산을 확보하십시오. 우리는 시드 예제와 고정된 필수 용어를 더한 꼬리 블록의 비용을 계산하고, 200토큰 예산에서 이를 뺀 다음, 남은 공간에 순위가 매겨진 용어집 용어만 허용합니다. 탐욕적으로 채우고 고정 항목을 마지막에 추가하는 순진한 순서는 가치가 낮은 용어가 예산을 소모하고 자체 보호 장치에 의해 고정 항목이 누락되는 실패 모드를 가집니다.

둘째, 중요도 오름차순으로 용어를 배치하여 문자열이 가장 중요한 용어들로 끝나도록 합니다. 예산 내에서는 이것이 아무것도 바꾸지 않습니다. 하지만 파이프라인의 구성 요소가 계산을 잘못하는 경우, 전방 절단(front-truncation)은 가장 덜 중요한 용어를 먼저 삭제합니다. 이는 최악의 치명적인 실패를 우아한 실패로 전환시킵니다.

우리 시스템의 최종 조립된 프롬프트는 순위가 매겨진 용어집 용어 25개, 그 다음 시드 예제, 그리고 맨 끝에 고정된 브랜드 이름 3개로, 총 약 170개의 추정 토큰입니다. 이 조사를 시작하게 한 두 건의 프로덕션 환경 오인식은 모두 사라졌으며, 이는 전후 원본 오디오 클립과 대조하여 검증되었습니다.

호스팅된 Whisper API에 대한 프롬프트 편향 체크리스트

  • 유효 프롬프트 한도는 224 토큰입니다. 공급자 측 문자 유효성 검사(저희가 사용한 API에서는 896자)는 별개의 훨씬 느슨한 제약 조건입니다. 토큰을 기준으로 예산을 책정하십시오.
  • 오버플로는 앞에서부터 조용히 잘립니다. 중요한 어휘는 프롬프트의 시작이 아닌 끝에 배치하십시오.
  • 위치별 A/B 테스트로 절삭 동작을 직접 확인하십시오: 한도 초과 프롬프트 하나, 중요 용어의 앞/뒤 배치 비교, 동일 오디오 사용.
  • 토크나이저 없이 토큰을 추정하는 경우, 스크립트별 계수를 사용하여 룬(rune) 단위로 스캔하고, 실제 토크나이저 출력에 대한 테스트를 통해 강제되는 상한값으로 설정하십시오. 음역된 외국 브랜드 이름은 사전 텍스트보다 토큰화 비용이 훨씬 더 많이 들 것으로 예상해야 합니다.
  • 필수 용어에 대한 예산을 먼저 확보하고, 나머지는 순위에 따라 채우며, 선택 과정을 결정적으로 유지하십시오: 고유 ID까지 이어지는 안정적인 정렬 키를 사용합니다.
  • 조립된 프롬프트별로 추정된 토큰 수를 기록하고, 추정치가 상한선에 가까워지면 알림을 보내십시오. API는 절대 알려주지 않을 것입니다.

불편한 요약은 편향 프롬프트가 문자열이 아니라 예산이 할당된 데이터 구조라는 것입니다. 고정 크기 버퍼를 다루는 것과 동일한 주의를 기울여 다루십시오. 모델이 그것을 그렇게 바꾸기 때문입니다.

관련 글