AI 에이전트가 실제 웹사이트에서 실패하는 이유

당신의 에이전트는 테스트에 쓴 페이지에서는 잘 돕니다. 그런데 열린 웹으로 돌리는 순간 답이 흐릿해지거나, 자신만만하게 틀리거나, 브라우저에서는 내용이 뻔히 보이는데 "페이지가 비어 있었다"고 말합니다.

본능적으로 프롬프트에 손이 갑니다. 대개 프롬프트는 멀쩡합니다. 고장은 모델이 무언가를 보기 전에 일어났습니다. URL을 가져와 텍스트로 바꾸는 계층에서요.

그 계층을 실제 페이지에서 측정했습니다. 깨지는 방식은 다섯 가지, 각각이 실제로 어떤 모습인지와 함께 정리했습니다.

1. JavaScript가 돌기 전까지 페이지에 글자가 없다

atlassian.com/software/jira/pricing을 평범하게 가져오면, 1.37MB의 HTML에서 읽을 수 있는 내용은 1 토큰입니다. 한 문단이 아니라 1 토큰, 줄바꿈 하나입니다.

가격은 그 안에 있습니다. <script> 안에 애플리케이션 상태로 들어앉아, 브라우저가 그것으로 페이지를 지어주기를 기다립니다.

같은 URL을 헤드리스 브라우저로 가져오면 3,048 토큰이 되고 이렇게 시작합니다.

## **Transparent pricing for every team.**

같은 URL. 같은 날. JavaScript를 실행했느냐 아니냐에 따라 0이거나 페이지 전체입니다.

이것이 모델 문제처럼 보이지만 아닌 고장입니다. 에이전트가 "이 페이지에는 가격이 언급되지 않았다"고 말한 건, 받은 바이트 안에서는 그게 사실이었기 때문입니다.

2. 그런데 렌더링은 바라던 해법이 아니다

당연한 대응은 전부 렌더링하는 것입니다. stripe.com/docs에서 해 보십시오.

FetchError: Rendered fetch failed: Navigation timeout of 25000 ms exceeded

단순 요청이면 1초도 안 걸려 돌아오는 페이지가, 헤드리스 브라우저에서는 25초 안에 로딩을 끝내지 못했습니다. 렌더링하지 않은 쪽은 흠이 있을지언정 적어도 돌아왔습니다.

렌더링의 대가는 페이지당 브라우저 프로세스, 수백 밀리초에서 수 초의 지연, 그리고 새로운 종류의 고장입니다. 타임아웃, 차단된 리소스, load를 끝내 발생시키지 않는 페이지. 기본값으로 렌더링하는 에이전트는 웹의 일부 구간에서 더 느리고 안정적입니다. 더 안정적이지 않습니다.

쓸 만한 형태는 이렇습니다. 값싼 요청을 먼저, 무언가 받았는지 확인하고, 비어서 돌아온 페이지에 대해서만 렌더링으로 올라가기. 바로 그 확인이 다들 빠뜨리는 부분입니다.

3. 쓰지도 않을 마크업에 돈을 내고 있다

우리가 측정한 열두 개의 유명 페이지에서, 서버에서 모델까지 살아남는 비율의 중앙값은 1%에 못 미칩니다. shopify.com/pricing은 410,515 토큰을 보내고 2,479를 만들어냅니다.

에이전트가 날 HTML을 컨텍스트 윈도에 밀어 넣는다면, 감싸는 <div>와 인라인 SVG에 입력 토큰 값을 치르면서 같이 읽으려던 페이지를 밀어내고 있는 것입니다. 보내기 전에 추출하십시오. 더 좋게는, 그 사이트가 Markdown을 바로 주는지 물어보십시오. 이미 주는 곳들이 있습니다. 시험한 열두 페이지 중 다섯이 어떤 형태로든 Markdown 표현을 제공했고, 그중 하나는 우리 것입니다.

4. 추출이 엉뚱한 부분을 남긴다

추출은 휴리스틱입니다. 본문을 찾고 가구를 버리는데, 본문 모양이 아닌 페이지에서는 잘못 짚습니다.

stripe.com/docs는 1,057자로 줄어들고 이렇게 시작합니다.

$ stripe payment_intents create --amount 1099 --currency "usd"

페이지에 실제로 있는 조각입니다. 동시에 Stripe가 무엇을 하는 회사인지 설명하는 문장이 아니라 터미널 예제입니다. 그것만 받은 에이전트는 코드 조각에서 제품을 추론해야 합니다.

그러니 추출이 성공했다는 사실을 추출이 쓸모 있다는 증거로 삼지 마십시오. 값싼 방어책 — 추출한 것에 문장 형태의 텍스트가 없거나, 0.5MB를 보낸 페이지에서 수백 자에도 못 미친다면, 사실로 모델에 넘기지 말고 읽기 실패로 처리해 다음 단계로 올리십시오.

5. 같은 URL이 같은 페이지를 주지 않는다

shopify.com/pricing을 단순히 가져오면 영어가 왔습니다. 동일한 기기에서 동일한 URL을 렌더링하니 스페인어가 왔습니다. 단순 요청이 Pay monthly€32 EUR/mo라고 하던 자리에 Pago mensual32 € EUR/mes가 있었습니다.

여기서 깨진 것은 없습니다. 헤드리스 브라우저는 우리의 단순 요청과는 다른 Accept-Language와 위치 신호를 보냈고, 다르지만 올바른 표현을 받았습니다. 그러나 URL만으로 캐시하거나 오늘의 읽기를 어제 것과 비교한다면, 이 차이는 사이트가 가격을 바꾼 것처럼 보일 것입니다.

Accept-Language를 명시적으로 보내십시오. 캐시 키는 실제로 요청한 내용으로 만드십시오.

일어나지 않은 고장

차단을 예상했습니다. 열두 페이지 모두를 두 번씩 조사해 — 한 번은 브라우저로, 한 번은 OAI-SearchBot으로 — Cloudflare, Vercel, CloudFront를 가로질러 상태 코드를 비교했습니다.

봇을 다르게 대한 곳은 하나도 없었습니다. 하나도요.

에이전트를 차단하는 곳이 없다는 뜻은 아닙니다. 차단하는 곳은 많고, robots.txt는 앞으로도 읽고 지켜야 합니다. 뜻하는 바는, 웹의 넓은 표본에서 에이전트가 실패하고 있다면 그 이유가 차단일 가능성은 낮다는 것입니다. 잠긴 문보다 빈 방이 훨씬 흔합니다.

웹과의 접촉을 견디는 읽기 계층

1. Markdown부터 요청     → Accept: text/markdown 또는 /page.md
2. 단순 요청             → 값싸고, JS 없이
3. 받은 것을 확인         → 문장 형태인가? 보낸 바이트에 걸맞은 길이인가?
4. 아니면 올리기          → 렌더링, 타임아웃과 예산을 붙여서
5. 정직하게 포기          → "이 페이지는 읽지 못했습니다"

5번은 보이는 것보다 중요합니다. 읽을 수 없는 페이지를 보고하는 에이전트는 디버깅할 수 있습니다. 모델에 줄바꿈 하나를 건네고 즉흥 연주를 시키는 에이전트야말로 당신의 가격을 지어내는 장본인입니다.

짧은 요약

  • 에이전트 실패의 대부분은 추론 실패가 아니라 읽기 실패입니다.
  • JavaScript 없이 비어 있음이 압도적으로 큰 원인이고, 렌더링이 그것을 고칩니다. 대가를 치르고, 늘 그런 것도 아니고요.
  • 믿기 전에 추출을 매번 검증하십시오.
  • 주목은 차단이 가져갑니다. 피해는 공백이 냅니다.

페이지가 어떻게 읽히는지 확인 · 에이전트가 실제로 보는 것 · 에이전트에게 Markdown 제공하기