하루 만에 동일한 탐지기를 세 번 다시 작성했습니다. 각 수정 사항은 새로운 방식의 오류를 불러왔습니다.
요약
LLM이 기업의 공식 웹사이트를 날조하는 사례를 탐지하기 위한 도구 개발 과정과 그 과정에서 겪은 시행착오를 다룹니다. 탐지 로직을 수정할 때마다 발생하는 새로운 오류와 상관관계가 있는 오류(correlated error)의 위험성을 설명합니다.
핵심 포인트
- LLM의 환각 현상 중 기업 공식 웹사이트를 잘못 지칭하는 사례 탐지 시도
- 엔진 간 불일치에 의존하는 탐지 로직은 상관관계가 있는 오류에 취약함
- 정답 데이터(ground truth)를 활용한 검증 방식의 도입과 한계
- 모델이 사용자에게 가짜 사이트를 경고하는 올바른 동작을 환각으로 오인할 위험성
지난주에 저는 LLM이 기업의 공식 웹사이트를 날조하는 사례에 대한 글을 썼습니다. 그 후, 더 많은 사례를 대규모로 찾아내기 위해 탐지기(detector)를 구축했습니다.
저는 현재 하루 만에 그 탐지기를 세 번 다시 작성했습니다. 각 버전은 이전 버전의 버그를 수정했지만, 동시에 동일한 데이터를 잘못 분류하는 새로운 방식을 도입했습니다. 탐지된 수치는 1 → 134 → 41 → 12로 변했습니다.
흥미로운 점은 최종 숫자가 아닙니다. 세 가지 버그 모두가 서로 다른 옷을 입은 동일한 버그였다는 점이며, 저는 두 번째 버전과 세 번째 버전 사이에 바로 그 버그에 관한 기사를 이미 발표했다는 사실입니다.
버전 1: 일치 여부에 눈이 멀다
탐지기의 임무: 브랜드가 소유하지 않은 도메인을 LLM이 브랜드의 "공식 웹사이트"라고 부르는 경우를 찾는 것입니다.
// v1
if (engineA.saysOfficial(domain) && engineB.saysNoSuchSite(domain)) {
flag(domain);
...
이 버전은 제가 발표했던 실제 사례 하나를 찾아냈습니다. 한 댓글 작성자가 허점을 지적했습니다:
불일치(disagreement)는 트리거라기보다는 확인(confirmation)에 가깝습니다. 엔진이 공식이라고 부르는 모든 URL은 교차 실행(cross run) 없이 단일 엔진만으로도 브랜드가 소유한 도메인 목록과 직접 대조해 볼 수 있습니다. 그리고 모든 엔진이 동일하게 잘못된 도메인을 지칭하는 경우, 즉 더 심각한 상황을 잡아낼 수 있을 것입니다.
그의 말은 모든 면에서 옳았으며, 세 번째 지적이 가장 심각합니다. 불일치(inconsistency)에 따라 작동하는 규칙은 상관관계가 있는 오류(correlated error)에 구조적으로 눈이 멀어 있습니다 — 이는 위험한 종류의 오류입니다. 왜냐하면 중첩된 코퍼스(corpora)로 학습된 모델들은 함께 실패하며, 바로 그때 잘못된 답변이 모든 사용자에게 동시에 도달하기 때문입니다.
제가 발표했던 유일한 발견은 6개의 엔진 중 하나가 우연히 반대 의견을 냈기 때문에 살아남을 수 있었습니다. 패널에서 그 엔진을 제외하면 v1은 아무것도 반환하지 못합니다.
버전 2: 134개의 발견, 대부분 오답
수정 방법은 명백해 보였습니다. 사용되지 않고 있던 정답 데이터(ground truth)가 있었습니다 — 모든 브랜드는 자신이 소유한 도메인 목록을 가지고 있습니다. 주장을 직접 확인하는 방식입니다:
// v2
for (const url of urlsIn(answer)) {
const h = hostname(url);
...
탐지된 도메인이 1개에서 134개로 늘어났습니다. 저는 상위 9개를 정리한 표와 함께 이 포스트의 초안을 작성하기 시작했습니다.
그다음 저는 출력값의 앞부분(head) 대신 나머지 부분(rest)을 살펴보았습니다. airtable-official.com, asana-vip.net, wrike-sales.net, basecampchina.net 같은 도메인들이 보였습니다. 실제 채널이 아니라, _조작된 예시(fabricated examples)_와 패턴이 일치하는 이름들이었습니다.
실제로 그러했습니다. 다음은 대표적으로 플래그(flagged)된 구절입니다:
检查域名:官方主站是
wrike.com。任何其他域名(如wrike-official.com、wrike-sales.net)都需要警惕。
(도메인 확인: 공식 사이트는 wrike.com입니다. wrike-official.com, wrike-sales.net 등 다른 도메인은 주의하십시오.)
엔진은 **사용자에게 가짜 사이트에 대해 경고(warning)**하고 있었습니다. 제 탐지기는 "공식 웹사이트(official website)"라는 단어 근처에 있는 도메인을 발견하고 이를 주장(claim)으로 기록했습니다. 저는 모델의 올바른 동작을 환각(hallucination)으로 점수 매기고 있었던 것입니다.
플래그된 세트를 샘플링해 보았습니다: 제가 가장 경악스러운 새로운 발견이라고 발표하려 했던 notion-cn.com은 86%의 확률로 경고(warning) 문맥 내에서 나타났습니다.
단언(Assertion), 경고(warning), 부정(denial)은 세 가지 상태입니다. 저는 두 가지만 가지고 있었습니다.
버전 3: 여전히 잘못된 도메인으로 귀속시킴
그래서 저는 3일 전 브랜드 언급을 위해 구축했던 극성(polarity) 로직을 재사용하여 세 번째 상태를 추가했습니다:
if (negationCues.test(tightClause(answer, i))) return "denied";
if (warningCues.test(block(answer, i, 260))) return "warned";
if (assertCues.test(clause(answer, i))) return "asserted";
...
41개가 플래그되었습니다. 나아졌습니다. 하지만 여전히 틀렸으며, 제가 그 주에 이와 관련된 글을 썼음에도 불구하고 예측할 수 있었던 방식으로 틀렸습니다.
아까 그 Wrike 구절을 다시 보십시오. 官方主站 ("공식 메인 사이트")라는 문구가 wrike-sales.net의 약 30자 앞에 나타납니다. 근접성 기반 규칙(proximity-based rule)은 이를 가장 가까운 도메인에 귀속시킵니다. 하지만 해당 큐(cue)는 그 앞에 이름이 언급된 도메인인 wrike.com에 속하는 것입니다. 문장 구조는 다음과 같습니다: [실제 도메인, 단언됨] 그 다음 [가짜 도메인들, 경고됨].
근접성(Proximity)은 귀속(Attribution)이 아닙니다. 이것은 제가 며칠 전 게시했던 것과 동일한 실패 사례입니다. 당시에는 브랜드 이름 주변의 키워드 윈도우(keyword window)가 인접한 브랜드에 속하는 감성(sentiment)을 포착하는 문제가 있었습니다. 동일한 버그였고, 엔티티 타입(entity type)만 달랐을 뿐이며, 저는 그대로 그 함정에 빠졌습니다.
버전 4: 마침내 구현한 귀속(attribution)
해결책은 단언 신호(assertion cue)와 분류 대상이 되는 도메인 사이에 다른 도메인이 위치하지 않도록 요구하는 것입니다:
const clause = tightClauseAround(a, index);
const m = ASSERT_CUES.exec(clause);
if (!m) return "referenced";
...
동일한 4,023개의 저장된 답변에 대한 최종 집계 결과입니다:
12개의 도메인이 공식적이라고 단언되었으나 소유되지 않음
173개의 도메인이 오직 경고 문맥에서만 나타남
두 번째 숫자가 우리가 깊이 생각해 봐야 할 지점입니다. 엔진이 정확히 옳은 일을 하고 있었던 173가지 사례 — 즉, 구매자들에게 유사 도메인(lookalike domains)을 주의하라고 알려주고 있었던 사례들 — 에 대해, 제 탐지기의 버전 2는 그 모든 사례를 환각된 공식 채널(hallucinated official channel)로 계산했습니다.
만약 제가 v2를 출시했다면, 저는 LLM이 134개의 가짜 공식 웹사이트를 만들어냈다는 이야기를 발표했을 것입니다. 하지만 실제 이야기는 LLM이 소수의 가짜를 만들어냈고 훨씬 더 많은 사례에 대해 경고를 했다는 것이며, 이는 사실상 정반대에 가깝습니다.
이번 경험을 통해 배운 점
세 가지 버그, 하나의 형태. 각 버전은 제가 _근처의 텍스트(nearby text)_를 _해당 대상에 관한 텍스트(text about this thing)_로 취급했기 때문에 실패했습니다. 브랜드 이름 주변의 윈도우, URL 주변의 절(clause), 도메인 주변의 블록 — 세 가지의 세밀도(granularities)에서 동일한 실수를 범했습니다. 엔티티 수준의 귀속(Entity-level attribution)이 실제 문제이며, 윈도우 크기를 아무리 조정해도 해결되지 않습니다. 이제 저는 제가 작성하는 모든 새로운 추출기(extractor)가 명시적으로 확인하기 전까지는 이 버그를 가지고 있다고 가정합니다.
출력의 앞부분을 읽는 것은 출력을 읽는 것이 아닙니다. 저는 이 포스트의 초안을 처음 50줄을 바탕으로 작성했습니다. 이야기가 무너진 곳은 뒷부분(tail)이었습니다. 빈도수(count)로 정렬하고 상위 항목을 읽는 것은 당신이 예상하는 바를 확인하는 방법입니다.
탐지 규칙(detection rule)에는 단순히 정탐(true-positive) 클래스뿐만 아니라, 오탐(false-positive) 클래스도 필요합니다. v2에는 "내가 찾고 있는 것과 유사한 올바른 동작"이라는 개념이 없었습니다. 일단 그 클래스의 이름을 붙이고 나니, 제가 플래그(flag)를 지정한 것의 93%가 바로 그 클래스였다는 사실이 드러났습니다.
이제 저는 추출기(extractor)의 수치를 신뢰하기 전에 다음과 같은 세 가지 질문을 던집니다:
- 모델이 X를 주장하는지, X에 대해 경고하는지, 아니면 X를 부정하는지를 구분할 수 있는가? 이들은 텍스트상으로는 거의 동일해 보이지만 의미는 정반대입니다.
- 엔티티(entity) 근처에서 단서(cue)를 발견했을 때, 그 단서가 옆에 있는 것이 아니라 해당 엔티티에 '관한' 것이라는 점을 무엇이 보장하는가?
- 당신이 탐지하려는 대상과 유사한 형태를 가진 정당한 대상(즉, 실패 패턴과 일치하는 올바른 동작)이 있다면, 그 유사한 대상이 테스트 케이스(test cases)에 포함되어 있는가?
저는 이러한 종류의 오류를 잡아내는 것이 유일한 목적인 코드베이스를 다루면서도, 단 하루 만에 이 세 가지를 모두 틀렸습니다. 유일하게 효과가 있었던 방법은 한 낯선 사람이 제가 고려하지 못한 사례를 제 규칙이 처리할 수 있는지 물어봐 준 것과, 요약본(summary) 대신 원시 출력(raw output)을 직접 읽은 것이었습니다.
Harness, 스코어링 모듈(scoring module), 그리고 라벨링된 검증 세트(labelled validation sets)는 CC BY 4.0 라이선스 하에 공개되어 있습니다: github.com/David88666/china-ai-visibility-benchmark
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기