AI 협업 개발

LLM 합의 게이트에서 무응답은 의견 불일치가 아니다

한 모델이 응답에 실패하는 것을 불일치로 간주하면 재시도 예산이 고갈되고 장애가 최종 판정으로 변질됩니다. 두 상태를 분리하는 방법입니다.

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

2개 모델 합의 게이트는 간단한 아이디어입니다. 즉, 두 개의 독립적인 LLM에 동일한 질문을 하고, 두 모델이 동의할 때만 조치를 취하며, 그 외 모든 것은 사람에게 전달하는 것입니다. 이 방법은 효과가 있으며, 저는 이전에 반드시 정확해야 하는 결정에서 합의가 단일 모델보다 나은 이유에 대해 글을 쓴 적이 있습니다. 이 게시물은 패턴 자체 내의 실패 모드에 관한 것입니다. 스크래핑된 용어 후보를 분류하는 파이프라인에서 불일치율은 몇 주에 걸쳐 18%에서 68%로 상승했으며, '모델이 동의할 수 없음'으로 자동화에서 영구적으로 제외되는 항목의 수가 점점 늘어났습니다. 이러한 항목 중 상당수는 전혀 불일치가 있었던 것이 아니었습니다. 모델 중 하나가 단순히 답변을 하지 않았고, 코드는 침묵을 불일치로 간주했습니다.

합의 게이트는 두 가지가 아닌 네 가지 결과를 가집니다

합의 게이트에 대한 순진한 멘탈 모델은 모델이 동의하거나 동의하지 않는 두 가지 결과를 가집니다. 실제 상태 공간은 더 크며, 추가적인 상태들이 바로 신뢰성이 누수되는 지점입니다.

  1. 두 모델 모두 답변했으며 판정이 동일합니다. 조치하기에 안전합니다.
  2. 두 모델 모두 답변했으나 판정이 다릅니다. 진정한 불일치이며, 재시도하거나 사람이 검토할 가치가 있습니다.
  3. 한 모델은 답변했고, 다른 모델은 답변하지 않았습니다. 당신은 두 개가 아닌 하나의 의견을 가지고 있습니다. 아무것도 비교되지 않았습니다.
  4. 두 모델 모두 답변하지 않았습니다. 이 배치에 대한 완전한 중단입니다.

상태 2와 3은 코드를 작성할 때 비슷하게 느껴질 수 있습니다. 두 경우 모두 진행할 수 없기 때문입니다. 하지만 그들은 정반대의 것을 의미합니다. 불일치는 데이터에 대한 신호입니다. 즉, 해당 항목이 정말로 모호하며 질문을 반복해도 수렴할 가능성이 낮다는 것입니다. 무응답은 타임아웃, 속도 제한, 콘텐츠 필터, 부분적인 파싱과 같은 인프라에 대한 신호입니다. 항목 자체는 아주 쉬울 수 있습니다.

대부분의 구현은 상태 4를 인코딩합니다. 완전한 중단은 요란하고 명백하기 때문입니다. 상태 3은 조용히 상태 2로 통합되는 경우이며, 그 통합이 바로 버그입니다.

코드에서 무응답이 불일치로 이어지는 과정

이러한 붕괴는 보통 맵 조회에서 발생합니다. 각 모델은 판정 배치를 반환하고, 이를 항목별로 인덱싱한 다음 비교합니다:

gv, gok := geminiVerdicts[item]
ov, ook := openaiVerdicts[item]
if !gok || !ook {
    return Judgment{Agreed: false} // missing answer, "not agreed"
}
if gv != ov {
    return Judgment{Agreed: false} // real disagreement
}

두 경로 모두 동일한 구조체를 생성합니다. 후속 처리에서 Agreed == falsedrop-disagree로 기록되고, 재시도 카운터가 올라가며, 세 번의 시도 실패 후 해당 항목은 소진됨으로 표시되고 자동화된 모집단에서 제거됩니다. 시스템은 로그에서조차 소진된 항목이 세 번 모호했는지, 아니면 세 번 운이 없었는지 알려줄 수 없습니다.

두 가지 세부 사항으로 인해 이 문제는 보기보다 더 심각합니다. 첫째, 부분적 파싱 손실이 흔하게 발생합니다. 15개 항목의 배치를 판단하도록 요청받은 모델이 때때로 오류 없이 14개를 반환하는 경우가 있습니다. 누락된 항목은 자신의 잘못 없이 상태 3이 됩니다. 둘째, 일방적인 실패는 시간적으로 상관관계가 있습니다. 제공자의 상태가 좋지 않은 밤에는 전체 배치가 한 번에 상태 3에 빠지고, 그 안의 모든 항목이 재시도를 소모합니다.

진짜 희생양은 재시도 예산

우리의 게이트는 각 항목에 세 번의 시도 기회를 주었습니다. 세 번의 실제 불일치는 "모델이 이 항목에 대해 일치된 결론을 내릴 수 없으니 사람이 확인해야 함"이라는 상황에 대한 합리적인 정의입니다. 재시도 예산은 모호성을 걸러내는 필터입니다.

그 예산에 무응답을 포함하여 계산하면 필터가 선택하는 대상이 조용히 바뀝니다. 불안정한 상태였던 두 번의 밤과 한 번의 실제 불일치를 겪은 항목은, 세 번 모두 모호했던 항목과 동일한 상태로 소진됩니다. 더 나쁜 것은, 지속적인 단방향 장애가 인프라 장애를 데이터 판정으로 직접 전환한다는 점입니다. 장애 동안 처리된 모든 항목은 소진을 향해 한 걸음씩 나아가고, 그런 밤이 세 번 지나면 파이프라인은 실제로는 전혀 평가하지 않은 수백 개의 항목에 대해 무언가 "결론"을 내리게 됩니다.

마침내 수치를 확인할 수 있게 되었을 때 우리의 데이터가 보여준 것이 바로 그것이었습니다. 불일치로 표시된 항목을 재시도했더니 약 3분의 1의 경우 결과가 바뀌었습니다(첫 "불일치" 후 재시도된 항목 중 181개는 나중에 합의된 거부로, 139개는 합의된 수락으로 해결되었고, 577개는 불일치 상태로 남았습니다). 이러한 불안정성 중 일부는 경계선상의 입력에 대한 모델의 실제 흔들림 때문이었습니다. 하지만 알 수 없는 비율만큼은 상태 2의 이름을 쓴 상태 3이었으며, 수정 전에는 로그 형식이 둘을 구별할 수 없게 만들었습니다. 코드는 각 모델이 무엇을 말했는지가 아니라 고정된 설명 문자열을 저장했습니다.

해결책: 답변이 아닌 것은 기록하되 집계하지 않기

변경 사항은 작으며 세 부분으로 구성됩니다.

첫째, 비교 이후에도 참 값이 유지되도록 judgment 구조체를 확장합니다.

type Judgment struct {
    Agreed    bool
    Oneside   bool   // at least one model gave no verdict
    GeminiOut string // raw verdict, "" means no answer
    OpenAIOut string
}

조기 반환 전에 모델별 필드를 채웁니다. 누락된 답변에 대해 조기 종료하기에 가장 자연스러운 위치는 해당 정보가 곧 손실될 위치이기도 하므로 순서가 중요합니다. 먼저 채운 다음 반환하세요.

둘째, 답변 없음에 자체 기록된 판정을 부여하고 재시도 횟수에서 제외합니다. 저희의 소진 규칙은 항목의 로그 행에 대해 max(attempt) >= 3이었습니다. 답변 없음 행을 attempt = 0으로 작성하면 소진에는 아무런 영향을 주지 않으면서 모든 감사 쿼리에 표시됩니다. 다음 실제 시도는 여전히 max(attempt) + 1로 그 번호를 계산하므로, 실제 시도 시퀀스는 끊어지지 않습니다.

셋째, 단측(one-sided)을 광범위하게 정의합니다. Oneside는 배치 전체가 반환된 한, 두 모델이 모두 누락된 경우를 포함하여 어느 한쪽 모델이라도 누락된 경우에 true여야 합니다. 두 모델 모두 동일한 항목을 누락한 배치는 불일치가 아니라 비교 불가입니다. 두 모델 모두 아무것도 반환하지 않은 경우를 위해 전체 장애 경로를 남겨두고, 이 경우에는 로깅을 완전히 건너뜁니다. 왜냐하면 작업이 완전히 중단된 밤에는 항목별 추적이 전혀 남아서는 안 되기 때문입니다.

두 모델 게이트의 네 가지 결과 두 모델 동의: 실행 불일치: 카운트 attempt = max + 1 누락: 기록 attempt = 0 3 스트라이크: 사람 대기열 // 실제 불일치만 재시도 예산을 소모합니다

포이즌 필과 3단계 에스컬레이션 가드

소진 처리에서 무응답을 제외하면 새로운 위험이 발생합니다. 만약 특정 입력이 한 제공자를 확실하게 침묵하게 만든다면(콘텐츠 필터가 전형적인 원인), 해당 항목은 이제 영원히 재시도됩니다. 그것은 매일 밤 모집단에 다시 들어가고, 두 번의 LLM 호출을 소모하며, 결코 해결되지 않습니다. 큐에 포이즌 필이 생긴 것입니다.

이 수정에 대한 해결책은 지속적인 단독 실패를 다시 카운트되는 경로로 에스컬레이션하는 것이며, 일반적인 중단 중에는 에스컬레이션이 작동하지 않도록 다음과 같은 보호 장치를 둡니다.

  • 전체 기간이 아닌, 슬라이딩 윈도우 내에서 무응답 행의 수를 셉니다. 우리는 14일을 사용했습니다. 1년 동안 한 달에 한 번 불안정한 밤을 겪은 항목은 포이즌 필이 아니며, 전체 기간 카운트는 어쨌든 결국 해당 항목을 에스컬레이션할 것입니다.
  • 에스컬레이션하기 전에 배치를 확인합니다. 만약 오늘 밤 배치의 대부분이 단독 실패라면, 그것은 항목의 속성이 아니라 제공자 사고입니다. 전체 배치에 대한 에스컬레이션을 억제하고 단순 무응답으로 기록합니다. 억제는 안전한 방향으로의 오류를 택합니다. 최악의 경우는 하룻밤 더 재시도하는 것뿐입니다.
  • 에스컬레이션을 할 때는, 기록된 판정을 실제 불일치와 동일하게 유지하여 모든 다운스트림 소비자(소진 쿼리, 대시보드, 드레인 추정치)가 이전과 똑같이 동작하도록 하고, 별도의 사유 열에 고정된 마커 문자열을 넣습니다. 나중에 항목을 읽는 사람은 "제공자가 계속 실패하여 이 항목이 자동화에서 제외되었습니다"라는 것을 볼 수 있어야 합니다. 왜냐하면 그에 대한 올바른 대응은 실제 모호성에 대한 올바른 대응과 다르기 때문입니다.

세 번의 시도 예산에 더해 세 번의 스트라이크 에스컬레이션을 사용하면, 포이즌 필의 최악의 경우 수명은 6일 밤으로 제한되며, 그 제한 기간 중 어떤 시간도 일어난 일에 대해 거짓말하는 데 사용되지 않습니다.

차이점을 볼 수 있게 되었을 때의 변화

눈에 보이는 성과는 즉각적입니다. 야간 통계가 "불일치(disagreed)"와 "일방적(one-sided)"으로 나뉘고, 파이프라인은 처음으로 좋지 않았던 한 주가 모델 품질 문제였는지 혹은 인프라 문제였는지를 답할 수 있게 됩니다. 변경 전에는 그 질문이 단순히 어려운 정도가 아니라 데이터로부터 답을 얻을 수 없었습니다. 증거가 있어야 할 자리에 로그 행이 상수 문자열을 저장했기 때문입니다. 각 모델의 원시 판정을 기록하는 데는 두 개의 열이 소요되며, 한 부류의 추측을 완전히 제거합니다.

언급할 가치가 있는 미묘한 회계 규칙도 있습니다. 즉, 분류기가 본 내용이 아니라 실제로 기록된 내용을 기준으로 통계를 보고해야 한다는 것입니다. '일방적(one-sided)'으로 도착했지만 집계 경로로 에스컬레이션된 항목은 오늘 밤 보고서의 '불일치(disagreement)' 열에 속합니다. 그것이 현재 데이터베이스가 해당 항목에 대해 말하는 바이기 때문입니다. 만약 집계와 저장된 행이 서로 달라지면, 모든 향후 디버깅 세션은 이 둘을 일치시키는 작업부터 시작됩니다.

자체 게이트를 위한 체크리스트

  • 코드에서 네 가지 결과(agree, differ, one-sided, outage)를 명시적으로 열거하세요. 만약 그중 두 개가 struct 값을 공유한다면, 이미 그것들을 병합한 것입니다.
  • early return 전에 모델별 원시 판정을 채우고 저장하세요. 누락된 답변에 대해 ""는 괜찮지만, 상수 설명 문자열은 그렇지 않습니다.
  • 무응답은 소진 규칙에서 제외하세요. max(attempt) 규칙 하의 attempt = 0 행은 집계하지 않고 기록하는 저렴한 방법입니다.
  • "대체로 정상인 배치 내에서 두 모델 모두 이 항목을 놓친 경우"는 disagreementoutage가 아니라 one-sided로 처리하세요.
  • poison pill을 제한하세요. 슬라이딩 윈도우 내에서 N개의 one-sided 실패가 발생한 후 에스컬레이션하고, 전체 배치가 실패하는 경우에는 에스컬레이션을 억제하며, 에스컬레이션된 행에 고유한 이유를 표시하세요.
  • 집계가 기록을 따르도록 만드세요. 통계는 선택된 분기(branch)가 아닌, 기록된 내용에서 계산되어야 합니다.

이 중 어떤 것도 LLM에만 국한된 것은 아닙니다. 신뢰할 수 없는 투표자가 있는 모든 투표 시스템은 반대(dissent), 부재(absence), 그리고 블랙아웃(blackout) 사이의 동일한 세 가지 구분을 가집니다. LLM 파이프라인은 타임아웃과 불일치가 모두 '오늘 밤 완료할 수 없는 비교'라는 동일한 것으로 도착하기 때문에, 단지 이를 놓치기 쉽게 만들 뿐입니다.

관련 글