인간보다 AI 에이전트가 먼저 읽도록 설계된 에너지 비교 서비스 구축하기
요약
비정형 PDF 고지서 파싱 문제를 해결하기 위해 기존의 규칙 기반 파서를 버리고 LLM을 입력 레이어로 활용하는 새로운 아키텍처를 구축한 사례를 소개합니다. LLM이 데이터를 추출하고, 내부 계산 엔진은 API와 MCP 서버를 통해 에이전트 친화적인 방식으로 호출되도록 설계되었습니다.
핵심 포인트
- 비정형 데이터 처리에 정규 표현식 대신 LLM을 활용하여 정확도 향상
- LLM을 입력 레이어로, 기존 로직을 도구로 사용하는 'Agent-shaped' 설계
- 에이전트의 효율성을 위해 컴팩트한 페이로드를 반환하는 API 엔드포인트 구축
- 개인정보 보호를 위해 LLM 단계에서 PII를 분리하고 숫자 데이터만 처리
저는 SwitchAI라는 작은 이탈리아 에너지 요금 비교 사이트를 운영하고 있습니다. 한동안 계획은 매우 평범했습니다. 사용자가 PDF 고지서를 업로드하면, 서버가 이를 파싱(Parsing)하고, 사이트가 더 저렴한 제안을 보여주는 방식이었죠. 그러다 이 계획을 5개의 서로 다른 이탈리아 공급업체에서 가져온 10개의 실제 고지서로 테스트해 보았는데, 제품 전체를 재편성하게 만들 정도로 완전히 무너져 버렸습니다.
무슨 일이 일어났는지, 그리고 양식을 채우는 "사용자"가 숫자를 입력하는 사람이 아니라 PDF를 들고 있는 LLM(대규모 언어 모델)이 되어가는 세상에서 제품을 구축한다는 것이 무엇을 의미하는지에 대한 제 생각을 공유하겠습니다.
파싱 문제가 아니었던 파싱 문제
이탈리아의 에너지 고지서는 표준 형식이 아닙니다. Enel, Octopus, A2A, NeN, Eni Plenitude는 각각 소비량, POD/PDR 코드, 비용 내역을 배치하는 방식이 너무 달라서, 한 공급업체에 맞춰 학습된 정규 표현식(Regex) 기반 파서(Parser)는 다른 공급업체에서 조용히 작동을 멈춰버립니다.
제 호스팅 환경은 이 문제를 더 악화시켰습니다. 공유 OVH 호스팅이라 pdftotext도 없고, Python 런타임도 없으며, OCR(광학 문자 인식)도 사용할 수 없었습니다. Enel 고지서에 대해서는 PHP 네이티브 파서를 꽤 잘 작동하게 만들 수 있었습니다. 하지만 구조가 완전히 다른 Octopus 고지서는 전혀 협조해주지 않았습니다.
더 많은 파싱 코드를 작성하기 전에, 일종의 검증(Sanity check)을 위해 동일한 10개의 PDF를 Claude, GPT, Gemini에 통과시켜 보았습니다. 세 모델 모두 제가 별도의 커스텀 로직을 작성하지 않았음에도 불구하고, 모든 공급업체의 형식에 대해 소비량, POD, 지출액, 지역을 10/10의 정확도로 올바르게 추출해 냈습니다.
그 순간 프로젝트의 아키텍처(Architecture)가 바뀌었습니다. 저는 제 코드보다 이미 더 잘하고 있는 영역에서 LLM을 파싱으로 이기려고 시도하는 것을 멈추고, 다른 질문을 던지기 시작했습니다. 만약 LLM이 "입력 레이어(Input layer)"가 되고, 저의 역할은 그 이후에 LLM이 호출할 수 있는 훌륭한 도구가 되는 것이라면 제품은 어떤 모습이어야 할까?
세 개의 정문, 하나의 계산 엔진
이제 SwitchAI는 모두 동일한 요금 비교 코어를 타격하는 세 가지 진입 경로를 가집니다:
사용자 → Claude/ChatGPT/Gemini에 고지서 업로드
↓
LLM → 소비량, 비용, 지역 추출 (그리고, 활성화를 원하는 경우 별도로 PII(개인 식별 정보) 추출)
...
REST 엔드포인트(endpoint), MCP 서버, 그리고 WebMCP 통합은 모두 내부적으로 동일한 PHP 계산 엔진을 호출합니다. ARERA(이탈리아 에너지 규제 기관)의 요금 계산 로직이 세 가지 구현체 사이에서 서로 일치하지 않고 어긋나는 것을 원치 않았기 때문입니다. 에이전트에게는 POST /api/analyze를 사용하도록 안내할 것입니다. 이 엔드포인트는 기존에 2~3번의 왕복(round trips)이 필요했던 과정을 단 한 번의 호출로 압축하며, 에이전트 친화적인(agent-shaped) 컴팩트한 페이로드(payload)를 반환합니다. 여기에는 최적의 제안, 자연어로 된 agent_summary, 절감액 상세 내역, 그리고 사용자에게 바로 전달할 수 있는 subscription_url이 포함됩니다.
내 API에서 개인 데이터를 분리하기
이 부분은 단순히 에너지 비교 사이트뿐만 아니라, 에이전트를 위한 도구를 구축하는 모든 사람에게 실제로 일반화(generalizable)될 수 있는 부분이라고 생각합니다.
이 도구는 이름, 주소, 또는 세무 코드(fiscal code)를 절대 수신하지 않습니다. 오직 숫자 — 소비량, 지출액, 지역 — 만을 수신합니다. LLM이 고지서에서 PII(개인 식별 정보)를 추출하여 자신의 컨텍스트(context) 내에 보유하고 있으며, 훨씬 나중에 클라이언트 측에서 미리 채워진(prefilled) URL을 생성할 때만 이를 사용합니다:
/sottoscrizione?tariff=ID&nome=Mario&cognome=Rossi&pod=IT001E...&consumi=2700
도구 설명(tool descriptions)에는 가드레일(guardrails)을 직접 포함합니다. 왜냐하면 에이전트가 행동하기 전에 실제로 읽는 유일한 "문서(documentation)"가 바로 그것이기 때문입니다:
- 사용자의 데이터는 활성화 용도로만 사용되며 세션 이후에는 저장되지 않음을 사용자에게 안심시킬 것
- URL에 개인 정보 필드를 포함하기 전에 명시적인 동의를 얻을 것
- 활성화가 완료되었다고 절대 주장하지 말 것 — 서비스 제공자에게 전달되기 전에 여전히 더블 옵트인(double opt-in) 확인 이메일이 필요함
이 중 어느 것도 스키마(schema)나 검증기(validator)에 의해 강제되는 것이 아닙니다. 대신, 도구 설명을 마치 매우 문자 그대로만 받아들이는 신입 사원에게 지시하듯 작성함으로써 강제합니다. 왜냐하면 실제로 도구를 호출하는 존재가 대략 그러하기 때문입니다.
존재하지 않았던 엔드포인트
제 코드를 멈춰 서서 다시 확인하게 만든 결정적인 순간은 바로 이것이었습니다. API 표면(API surface)에 대한 보안 검토를 진행하던 중, submit_subscription 엔드포인트에 대한 매우 확신에 차고 상세한 설명을 발견했습니다. 이는 추측이 아니라 마치 공식 문서처럼 읽히는 설명이었습니다. 하지만 이 엔드포인트는 제 라우트(routes), 코드베이스, 혹은 제가 실제로 배포한 그 어떤 곳에도 존재하지 않았습니다. 나머지 흐름이 마치 그것이 존재해야만 하는 것처럼 보였기 때문에, 모델은 매우 그럴듯하고 확신에 찬 방식으로 이를 추론해낸 것이었습니다.
이로 인해 악용 가능한 결과가 발생하지는 않았지만, 이는 진정으로 새로운 범주의 위험을 예고합니다. 즉, 모델이 명백히 틀린 사실을 말하는 것이 아니라, 여러분의 API 표면 중 일부를 매우 확신에 차서 그럴듯하게 발명해내는 위험입니다. 만약 에이전트(agent)를 대상으로 하는 무언가를 구축하고 있다면, 신중한 분석처럼 들리는 주장까지 포함하여, 여러분의 시스템에 대한 모든 확신에 찬 주장들에 대해 "이것이 실제로 내 코드에 존재하는가"를 검증하는 것이 이제 업무의 일부가 되었습니다.
검색 엔진이 아닌 에이전트를 위한 디스커버리 (Discovery)
전통적인 기술적 SEO (Technical SEO)는 여전히 중요합니다. 캐노니컬 URL (canonical URLs), 실제 사이트맵 (sitemap), 그리고 내용이 빈약한 자동 생성 페이지에 대한 noindex 설정 등이 그러합니다 (SwitchAI는 도어웨이 페이지(doorway-page) 페널티를 피하기 위해 373개의 인덱싱된 제공업체 페이지를 보유하고 있으며, 오퍼 상세 페이지는 의도적으로 인덱싱하지 않도록 설정했습니다). 하지만 저는 두 번째의 평행한 디스커버리 계층을 똑같이 중요하게 다루고 있습니다:
llms.txt— 이를 지원하는 모델들을 위해 사이트를 평이한 언어로 설명한 파일webmcp.json+ 등록된 WebMCP 도구들 — 이를 통해 Chrome의 브라우저 내 에이전트 도구가 사이트를 직접 찾아 호출할 수 있도록 함openapi.json— ChatGPT의 Actions (또는 OpenAPI를 사용하는 다른 모든 것들)가 한 번에 API를 가져올 수 있도록 함robots.txt— 많은 보일러플레이트(boilerplate) 설정들이 기본적으로 거부(default-deny) 방식을 취하는 것과 달리, ClaudeBot, GPTBot, Google-Extended, PerplexityBot, 그리고 anthropic-ai를 명시적으로 허용함
이 중 추가하기 복잡한 것은 하나도 없습니다. 또한 이 카테고리의 유사한 사이트들 중 거의 어느 곳도 아직 이를 수행하고 있지 않은데, 이는 주의력만 기울이면 얻을 수 있는 비용 없는 선점자 우위(first-mover advantage)라는 기묘한 기회입니다.
오늘 이 일을 시작하려는 사람에게 해주고 싶은 말
- 만약 LLM (Large Language Model)이 당신의 파이프라인 중 일부(제 경우에는 비정형 데이터 추출)를 이미 매우 훌륭하게 수행하고 있다면, 그 가정이 일시적일 것이라는 전제하에 구축하는 것을 멈추세요. 대신 그 사이의 이음새(seams)를 구축하세요.
- 가능한 한 도구의 입력 표면(input surface)에서 개인 데이터를 제외하세요. 에이전트(agent)가 데이터를 운반하게 하고, 인간이 검증하는 마지막 단계에서만 시스템과 다시 결합하도록 하세요.
- 도구 설명을 매우 유능하지만 매우 문자 그대로만 받아들이는 신입 사원을 위한 사양서(spec)처럼 작성하세요. 기능적으로 볼 때, 그 특정 텍스트의 대상(audience)은 바로 그들이기 때문입니다.
- 에이전트가 가끔 존재할 법한 엔드포인트(endpoint)를 스스로 만들어낼 것이라고 가정하세요. 실제로 무엇이 작동하는지에 대해 명확하고 지루할 정도로 확실한 단일 진실 공급원(source of truth)을 마련해 두세요.
llms.txt/webmcp.json/openapi.json을 10년 전의sitemap.xml처럼 취급하세요. 필수 사항은 아니지만 추가 비용이 거의 들지 않으며, 점차 당신의 트래픽 중 상당 부분이 이곳에서 발생하게 될 것입니다.
전 과정을 처음부터 끝까지 확인하고 싶다면: 웹사이트는 switchai.it에 있으며, MCP 서버는 npm과 GitHub에 있습니다. Claude에서 커넥터(Settings → Connectors → https://www.switchai.it/mcp)로 직접 추가할 수 있습니다.
이 중 어떤 부분이라도 더 깊이 있게 다루고 싶다면 언제든 환영입니다. 특히 ARERA 비용 계산 로직은 별도의 포스팅을 통째로 작성할 수 있을 만큼 규제 관련 예외 사례(edge cases)라는 토끼굴(rabbit hole)이 깊게 파여 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기