200 OK가 읽기 성공을 의미하지는 않습니다
요약
AI 에이전트가 웹 페이지를 가져올 때 HTTP 200 응답이 실제 콘텐츠 읽기 성공을 보장하지 않는 문제를 다룹니다. FetchGate를 통해 데이터 수집의 성공 여부를 결정론적으로 판정하여 에이전트의 허위 답변(hallucination)을 방지해야 함을 강조합니다.
핵심 포인트
- HTTP 200 응답이 실제 유효한 콘텐츠 수집을 의미하지 않음
- 빈 페이지나 차단 화면을 읽었을 때 에이전트가 추측으로 답변하는 위험성
- 수집 성공 여부를 RETRIEVED/FAILED/UNKNOWN으로 명확히 구분 필요
- 사후 평가 방식이 아닌 데이터 수집 단계에서의 강제 실패(hard-fail) 처리 중요
요약 (TL;DR): 당신의 AI 에이전트는 실제로 읽지도 않은 페이지에 대해 아주 태연하게 답변할 것이며, 트랜스크립트(transcript)는 이를 알려주지 않을 것입니다. HTTP 200은 바이트(bytes)가 도착했음을 의미할 뿐, 페이지를 제대로 가져왔음을 의미하지는 않습니다. FetchGate는 "우리가 실제로 읽었는가?"라는 질문을 결정론적인 RETRIEVED / FAILED / UNKNOWN 판정으로 전환하며, 모델이 허세를 부리기 전에 강제로 실패(hard-fail) 처리합니다.
AI 에이전트에게 웹 도구(web tool)를 주고, 페치(fetch)가 깔끔하게 실패하는 대신 조용히 실패할 때 어떤 일이 일어나는지 지켜보십시오. 페이지가 빈 자바스크립트 셸(JavaScript shell), Cloudflare의 "잠시만 기다려 주세요(just a moment)" 화면, 또는 200으로 위장한 403 오류로 돌아오더라도 에이전트는 이를 알아차리기 위해 멈추지 않습니다. 에이전트는 어쨌든 자신이 이미 가중치(weights)에 가지고 있던 정보로 답변하며, 트랜스크립트의 그 어떤 것도 에이전트가 페이지를 읽지 못했다는 사실을 알려주지 않습니다. 당신이 얻게 되는 것은 검색(retrieval)의 탈을 쓴 자신만만한 추측뿐입니다.
저는 계속해서 이 형태의 문제로 돌아오게 됩니다: 하나의 도구가 한 가지 질문에는 자신 있게 답하지만, 두 번째의 더 중요한 질문은 답하지 못한 채 남겨두는 상황 말입니다. 이번에 다룰 두 번째 질문은 제가 다뤄온 것 중 가장 작으면서도 가장 날카로운 질문입니다: 우리가 실제로 페이지를 읽었는가, 예 혹은 아니오?
가시적인 축: 토큰 (tokens)
이 문제의 대중적인 버전은 비용에 관한 것입니다. 원문 기사는 수만 개의 토큰(tokens)이 될 수 있으므로, 사람들은 이를 측정하고, 불평하며, 이를 줄이기 위해 더 깔끔한 추출(extraction) 방식이나 스텔스 브라우저(stealth browsers)를 찾습니다. 그 비용은 실재하며, 현재 처리되고 있는 문제입니다.
하지만 비용은 결코 위험한 부분이 아니었습니다. 컨텍스트 윈도우(Context windows)는 커졌고, 추출 기술은 향상되었으며, 비용 문제는 대부분 해소되었습니다. 위험한 부분은 그 아래에 내내 자리 잡고 움직이지 않았습니다: 바로 페치(fetch)가 읽기(read)를 실제로 생성했는지 여부입니다.
비가시적인 축: 페치(fetch)가 실제로 발생했는지 여부
실패 자체는 단순합니다. 질문에 답하기를 기다리는 동안 모델에게 아무런 콘텐츠도 전달되지 않으면, 모델은 그 공백을 채웁니다. 그것이 모델이 하는 방식이며, 이는 오작동이 아닙니다. 버그는 모델이 추측하는 것이 아니라, 빈 검색(retrieval) 결과가 근거가 있는 듯한 최종 답변에 도달하도록 허용하는 하네스(harness)에 있습니다.
이는 세 가지 방식으로 나타납니다:
- fetch(가져오기)가 아무것도 반환하지 않아 모델이 자신의 학습 가중치(training weights)로부터 답변하거나,
- fetch(가져오기)가 아무것도 반환하지 않아 모델이 우연히 보유하고 있던 오래된 이전 정보(stale prior)로부터 답변하거나,
- fetch(가져오기)가 기술적으로는 성공했지만 잘못된 콘텐츠(챌린지 페이지, 동의 벽, 소프트 404)를 반환하여, 모델이 그것을 읽고 마치 기사를 읽은 것처럼 답변하는 경우입니다.
제가 여기서 구축하고 있는 것은 새로운 근거 설정(groundedness) 작업이 아니며, 유사한 분야의 작업들에 대해 솔직해지고 싶습니다. RAGAS나 TruLens와 같은 RAG 충실도 검사(faithfulness checks), 자신이 무엇을 모르는지 파악하는 절제(abstention) 작업, 그리고 Perplexity의 "이 소스에 접근할 수 없습니다"와 같은 인용 UX는 모두 이 문제 근처에 위치하며 각자의 역할을 다하고 있습니다. 하지만 이들은 사후에 도착한 텍스트가 무엇이든 평가할 뿐입니다. 그들 중 어느 것도 의도한 리소스의 fetch(가져오기)가 실제로 발생했음을 단언하지 않습니다. 그 좁은 차이가 바로 핵심입니다. 즉, 근거 설정(grounding)의 상류(upstream) 단계인 fetch(가져오기) 경계에서, 모델이 답변을 하기 전에 엄격하게 실패(hard-fail)하도록 하는 검사입니다.
실수의 명칭
이 실수는 HTTP 200을 증거로 취급하는 것입니다. 200은 바이트(bytes)가 도착했음을 의미할 뿐, 그 바이트가 당신이 요청한 페이지인지 아니면 그 앞에 서 있는 벽인지에 대해서는 아무것도 말해주지 않습니다. JavaScript 앱은 본문이 거의 전부 스크립트이고 읽을 수 있는 텍스트가 거의 없는 상태로 200을 반환합니다. Cloudflare는 "당신이 인간인지 확인하십시오"라는 전체 페이지와 함께 200을 반환합니다. 두 경우 모두 모든 단순한 검사를 통과하며, 이것이 질문이 "요청이 성공했는가"가 아니라 "우리가 페이지를 얻어냈는가"가 되어야 하는 이유입니다.
필드가 아닌 게이트(Gate)
명백한 해결책은 모델에게 상태 코드, 바이트 수, 페이지가 비어 있는 것 같다는 메모와 같은 더 많은 메타데이터(metadata)를 전달하는 것입니다. 하지만 이는 작동하지 않습니다. 모델이 읽을 수 있는 더 풍부한 필드는 모델이 무시할 수 있는 더 풍부한 필드이기도 하기 때문입니다. 모델은 빈 본문을 무시하는 것과 똑같은 방식으로 바이트 수가 0인 것을 그냥 지나칠 것입니다. 만약 모델에게 페이지를 읽었는지 여부에 대한 투표권을 준다면, 당신은 애초에 신뢰하지 않았던 구성 요소에게 다시 결정을 넘겨주는 셈이 됩니다.
따라서 결정은 모델 외부인 오케스트레이션 계층 (orchestration layer)에서 결정론적 게이트 (deterministic gate)로서 존재합니다. 이 게이트는 페치 런타임 (fetch runtime)이 측정한 값을 읽고 다음 세 가지 판결 중 하나를 반환합니다:
- RETRIEVED: 실제적이고 충분한 콘텐츠가 있음을 지칭할 수 있습니다. 모델은 이에 대해 답변하고 인용할 수 있습니다.
- FAILED: 4xx 또는 5xx 에러, 타임아웃, HTTPS에서 HTTP로의 다운그레이드, 또는 빈 본문 (empty body)과 같이 읽기에 실패한 명확한 이유를 지칭할 수 있습니다. 에이전트 (agent)는 중단됩니다.
- UNKNOWN: 어느 쪽으로도 분류할 수 없으며, 종종 페이지가 렌더링이 필요한 자바스크립트 앱 (JavaScript app)인 경우에 해당합니다. 에이전트는 중단되거나, 렌더링 후 다시 확인합니다.
세 가지 계층이 판결을 제공하며, 그중 가장 엄격한 것이 승리합니다. 전송 계층 (transport layer)은 상태 코드와 리다이렉트 체인 (redirect chain)을 확인합니다. 추출 계층 (extraction layer)은 읽기 가능한 텍스트를 원시 바이트 (raw bytes)와 분리하여, 17바이트의 가격 JSON은 읽기에 성공한 것으로 간주하는 반면 400KB의 스크립트 셸 (script shell)은 그렇지 않도록 합니다. 세 번째 계층인 콘텐츠 유효성 지문 (content-validity fingerprint)은 차단벽 (walls)을 감시합니다. 이는 시간이 지남에 따라 노후화될 수 있는 최선의 노력에 기반한 휴리스틱 (heuristic)이므로, 판결을 UNKNOWN으로 낮추는 용도로만 허용됩니다. 이는 결코 읽기 성공을 만들어낼 수 없으며, 확정적인 FAILED로 가는 유일한 경로는 전송 신호뿐입니다. 게이트는 닫힌 상태로 실패하거나 라벨이 붙은 채로 실패하며, 결코 유창하게 실패하지 않습니다.
브라우저가 제 역할을 하는 곳
정적 리더 (static reader)에게 진정으로 비어 있는 페이지의 경우, 게이트는 실제 헤드리스 브라우저 (headless browser)로 격상하여 페이지를 렌더링하게 하고 다시 분류합니다. 핵심은 회피하는 것이 아니라 렌더링하는 것입니다. 정직한 렌더링 후에도 여전히 차단벽이 있는 페이지는 UNKNOWN 상태로 유지되며 확정적으로 실패합니다. 게이트는 사람이 하는 방식대로 브라우저 탭을 엽니다. 사이트가 의도적으로 세운 통제 장치를 무력화하지는 않습니다.
출처 (Provenance)는 보안 속성입니다
답변의 품질(Answer quality)은 관심을 가져야 할 명백한 이유이지만, 가장 날카로운 이유는 아닙니다. 에이전트(Agent)는 읽은 내용을 바탕으로 행동하며, 이는 조용한 가져오기 실패(silent fetch failure)가 버그일 뿐만 아니라 공격 표면(attack surface)이 될 수 있음을 의미합니다. 만약 제가 당신의 에이전트가 수행하는 가져오기(fetch)를 조용히 실패하게 만들거나, 미끼(decoy)를 제공할 수 있다면, 저는 에이전트가 다음에 무엇을 할지 유도할 수 있습니다. 검색 억제(Retrieval suppression), 은폐(cloaking), 그리고 컨텍스트 오염(context poisoning)은 모두 200 상태 코드를 신뢰하는 에이전트에 대한 공격입니다.
그렇기 때문에 모든 결정에는 영수증(receipt)이 함께 제공됩니다. 즉, 리다이렉트(redirect) 후의 최종 URL, 상태 코드(status), 가공되지 않은 바이트(raw bytes) 대 추출된 바이트(extracted bytes), 그리고 콘텐츠 해시(content hash)가 포함됩니다. 영수증은 그것이 증명하는 내용만큼만 강력하므로, 두 가지 정직한 한계를 가집니다. 해시는 읽은 내용을 고정하여 나중에 해당 주장이 재검증 가능하게 만들지만, 그것이 변조 방지(tamper-evidence) 기능은 아닙니다. 은폐된 페이지도 해시는 깔끔하게 생성되기 때문입니다. 또한 출처(provenance)가 곧 인증(authenticity)은 아닙니다. 이 관문은 '빈 내용에 대해 조용히 실패하는(fail-silent-on-empty)' 허점은 막아주지만, 지문(fingerprint)을 통과해 버린 잘못된 콘텐츠를 가져오는 데 성공하는 경우에는 대응하지 못합니다. 이는 기준을 "증거 없음"에서 "검사를 통과한 증거"로 높여줍니다. 이는 가치가 있는 일이지만, 그것이 전부는 아닙니다.
세 번째 한계가 있습니다: 읽기(read)가 곧 답변(answer)은 아니라는 점입니다. RETRIEVED(검색됨)는 충분한 콘텐츠가 도착했음을 의미할 뿐, 당신이 필요로 하는 사실을 포함하고 있다는 뜻은 아닙니다. 이를 확인하는 것은 근거 확인(groundedness) 작업이며, 이는 관문이 자신의 임무를 완수한 후 하류(downstream) 단계에서 구성됩니다.
밑바탕에 깔린 패턴
이것은 제가 동일한 형태를 구축한 세 번째 사례입니다. ReachGate는 사람이 발견 사항을 확인하기 전에 취약점이 코드 내에서 실제로 도달 가능한지(reachable)를 결정합니다. TrustGate는 데이터 뒤에 있는 실시간 상태를 고려했을 때 에이전트가 위험한 행동을 취해도 되는지를 결정합니다. FetchGate는 에이전트가 페이지를 바탕으로 답변하기 전에 해당 페이지가 실제로 읽혔는지를 결정합니다.
도메인은 다르지만 아이디어는 동일합니다: 결정론적 엔진(deterministic engine)이 판단을 내리게 하고, 모델은 오직 그것을 설명하게 하며, 모든 결정을 당신이 직접 확인할 수 있는 증거로서 제공하는 것입니다.
FetchGate는 세 가지 중 가장 작은 것으로, 읽을 것인지 말 것인지에 따라 거의 1비트 수준으로 내려가는 게이트입니다. 하지만 fetch boundary(가져오기 경계)는 에이전트가 현실을 파악하는 능력이 가장 희박해지는 지점이며, 나머지 두 가지가 커버하지 못하는 유일한 곳입니다. 이는 오픈 소스(open source)이며 결정론적(deterministic)이고, 모델이나 API 키 없이 실행되며, 조작된 읽기 시도를 게이트를 통과해 몰래 넘기려는 테스트와 함께 제공됩니다. 이 위조 시도는 설계상 반드시 적발됩니다. 200은 읽기가 아니며, 그 차이를 구분하지 못하는 에이전트는 매번 본 적도 없는 페이지를 읽었다고 아주 태연하게 당신에게 말할 것입니다.
설치하기
pip install "fetchgate[mcp]"
이것은 오픈 소스(open source)이며 결정론적(deterministic)입니다. 만약 이것이 단 하나의 조용한 잘못된 답변이라도 막아준다면, repo에 별(star)을 남겨 다른 사람들이 이를 찾는 데 도움을 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기