AI 모델 비교 페이지를 구축한 방법: 쌍별 그룹화(pairwise grouping), Claude 콘텐츠, libSQL 중복 제거(dedup)
요약
AI 모델 비교 페이지를 대규모로 자동 생성하기 위한 파이프라인 구축 과정을 다룹니다. Claude Haiku를 활용한 경제적인 콘텐츠 생성과 태그 기반 그룹화를 통한 효율적인 모델 페어링 전략을 설명합니다.
핵심 포인트
- 사용자의 구체적인 검색 의도를 충족하기 위한 비교 페이지 구축
- Claude Haiku를 활용하여 저비용으로 고품질 JSON 데이터 생성
- 태그 기반 그룹화로 110만 개의 조합 중 유의미한 비교 쌍 선별
- 단순 생성이 아닌 데이터 기반의 정직하고 구체적인 콘텐츠 생성 지향
저의 AI 도구 디렉토리(aiappdex.com)는 개별 모델 페이지와 함께 출시되었습니다. HuggingFace 모델당 하나의 페이지를 할당하여 다운로드 횟수, 태그, 그리고 Claude가 생성한 장단점을 포함했습니다. 그것이 최소 기능 제품(Minimum Viable Product, MVP) 수준의 디렉토리였습니다. 하지만 디렉토리 사용자들은 모델 이름을 검색하기만 하는 것이 아니라, 두 모델을 나란히 놓고 비교하기도 합니다. "로컬 추론(local inference) 프로젝트를 위한 Llama 3 vs Mistral 7B". "내 파인튜닝(fine-tuning) 예산에 맞는 텍스트 분류(text-classification) 체크포인트는 무엇인가". 비교 페이지는 상세 페이지와는 다른 의도를 충족하며, 저는 이를 대규모로 생성하기 위한 파이프라인을 구축했습니다.
이 글에서는 파이프라인이 어떻게 작동하는지, 어떤 실패 모드(failure modes)가 있는지, 그리고 만약 처음부터 다시 시작한다면 무엇을 바꿀 것인지에 대해 다룹니다.
왜 비교 페이지인가
비교 페이지를 만드는 근거는 검색 의도(search intent)에 있습니다. "[모델 A] vs [모델 B]"를 입력하는 사용자는 "[모델 A]"를 입력하는 사용자보다 평가 단계에서 더 진전된 상태입니다. 그들은 이미 어떤 모델들을 고려하고 있는지 알고 있으며, 구조화된 비교를 원합니다. 이는 더 구체적인 의도 신호입니다.
자동 생성에 반대하는 논거는 품질입니다. 만약 제가 Claude에게 구체적인 정보가 없는 두 모델을 비교하라고 요청하여 그럴듯하게 들리는 일반적인 내용의 단락을 만들어내게 한다면, 저는 가치를 더하는 것이 아니라 그저 그럴싸한 상용구(boilerplate)를 생산하는 것에 불과합니다. 프롬프팅(prompting)은 유용할 만큼 충분히 구체적이거나, 적어도 적극적으로 오도하지 않을 만큼 정직한 콘텐츠를 생성해야 했습니다.
제가 진행 중인 실험은 월 예산이 제한적(~$25)이기 때문에, 비교 생성 과정은 경제적이어야 했습니다. 저는 이 단계에서 Claude Haiku를 사용하고 있는데, 이는 제가 세 개의 디렉토리 ETL 전체에서 Claude Haiku를 사용하는 이유와 같습니다. 빠르고, 저렴하며, 프롬프트가 제약되었을 때 잘 형성된 JSON을 안정적으로 생성하기 때문입니다.
페어링(pairing) 문제
데이터베이스에 약 1,500개의 모델이 있어, 가능한 쌍(pair)의 수는 대략 110만 개에 달합니다. 100만 개의 비교 데이터를 생성하고 캐싱하는 것은 불가능합니다. 저는 이를 다룰 수 있고 일관성 있는 수준으로 줄여야 했습니다.
첫 번째 직관: 다운로드 수로 페어링하기. 전 세계적으로 상위 N개의 모델을 선택하면 Llama vs Mistral, bert-base-uncased vs roberta-base와 같이 인지도가 높은 쌍을 얻을 수 있습니다. 이는 인기 있는 쿼리에 대해 몇 가지 방어 가능한 비교를 생성하지만, 롱테일 (long tail)을 거의 완전히 무시하게 됩니다. 제 디렉토리는 대형 애그리게이터(aggregators)들이 집중하지 않는 롱테일 (long tail)을 위해 특별히 구축되었습니다.
최종 결정: 파이프라인 태그 (pipeline tag)로 그룹화한 후, 각 그룹 내 상위 4개를 페어링하기. HuggingFace의 파이프라인 태그 (pipeline tag)는 대략적으로 작업 카테고리에 대응합니다: text-generation, text-classification, image-to-image, token-classification 등입니다. 텍스트 분류 (text-classification) 모델을 평가하는 사용자는 동일한 조직의 이미지 생성기가 아니라, 다른 텍스트 분류 (text-classification) 모델과의 비교에 관심을 가집니다.
const byPipe = new Map<string, typeof models>();
for (const m of models) {
if (!m.pipeline_tag) continue;
...
상위 4개의 모델이 있는 파이프라인 태그 (pipeline tag)의 경우, 6개의 쌍(4개 중 2개를 선택)이 생성됩니다. 데이터베이스 내의 약 30개 활성 파이프라인 태그 (pipeline tag) 전체를 고려하면 총 약 180개의 쌍이 생성되는데, 이는 관리 가능한 수준이며 각 쌍이 적어도 동일한 사용자에게 개연성 있게 관련될 수 있는 범위입니다.
저는 COMPARE_LIMIT 환경 변수를 사용하여 실행당 총 쌍의 수를 50개로 제한합니다. 대부분의 실행에서는 이미 데이터베이스에 존재하는 쌍들이 많기 때문에 대부분의 쌍을 건너뜁니다.
폴백 템플릿을 사용한 Claude Haiku 생성
생성 단계에서는 Haiku에게 구조화된 JSON 비교를 요청합니다. Claude JSON 추출에 대한 저의 일반적인 접근 방식이 이 파이프라인에 그대로 적용됩니다: 시스템 프롬프트 (system prompt)를 단일 JSON 스키마 (JSON schema)로 제한하고, 응답을 매칭 및 파싱(match-and-parse)하며, 실패 시 템플릿으로 폴백(fallback)합니다.
시스템 프롬프트 (system prompt):
기술 디렉토리를 위해 두 개의 AI 모델을 비교합니다. 두 모델의 이름과 메타데이터가 주어지면, 구조화된 비교 결과물을 생성하세요. 오직 JSON 객체만 출력해야 합니다:
{ "summary": "2문장으로 된 개요", "differences": ["주요 차이점에 대한 3-5개의 불렛 포인트"], "similarities": ["공통점에 대한 2-3개의 불렛 포인트"], "recommendation": "어떤 상황에서 무엇을 선택해야 하는지에 대한 1-2문장의 가이드" }
구체적이고 개발자 중심적이어야 합니다. 마크다운을 사용하지 마세요. JSON 이외의 설명 문구도 작성하지 마세요.
사용자 프롬프트(user prompt)는 두 모델의 이름, 제작자, 파이프라인 태그(pipeline tags), 그리고 데이터베이스에 저장된 기존 요약 문자열(각각 최대 400자로 제한)을 전달합니다. 이를 통해 Haiku는 아키텍처 세부 사항을 환각(hallucinate)할 필요 없이 구체적인 입력값을 얻을 수 있습니다.
폴백(fallback) 템플릿은 훌륭한 콘텐츠는 아니지만, 틀린 콘텐츠도 아닙니다:
const fb: CompareData = {
summary: `${a.name}와 ${b.name}는 모두 ${a.pipeline_tag} 모델입니다. 자세한 내용은 각 항목을 참조하세요.`,
differences: ["아키텍처 및 사용 사례에 대해서는 개별 모델 페이지를 참조하세요."],
...
이 템플릿은 자신이 알지 못하는 부분에 대해 솔직하게 기술합니다. 페이지는 렌더링되고 구조는 유효하며, 만약 작동하는 API 키를 사용하여 파이프라인을 다시 실행한다면, (다음에 설명할) 캐싱 레이어(caching layer)가 깔끔하게 교체를 처리합니다.
pair_slug UNIQUE 키를 사용한 libSQL 캐싱
이 파이프라인에서 가장 중요한 설계 결정은 중복 제거(dedup) 레이어입니다. 모든 쌍(pair)은 결정론적인 슬러그(slug)를 가집니다:
const pairSlug = [a.slug, b.slug].sort().join("--vs--");
결합(join)하기 전에 정렬(sorting)을 수행함으로써, 루프 반복에서 어떤 모델이 먼저 나타나느냐에 관계없이 model-a--vs--model-b와 model-b--vs--model-a가 동일한 키를 생성하도록 보장합니다. 이 슬러그는 model_compare 테이블의 UNIQUE 컬럼입니다.
어떤 쌍에 대해 Claude를 호출하기 전에, 파이프라인은 다음을 확인합니다:
const existing = await db.execute({
sql: `SELECT 1 FROM model_compare WHERE pair_slug = ?`,
args: [pairSlug]
...
기존 행은 절대 다시 생성되지 않습니다. 이 덕분에 일일 크론(cron) 작업은 별도의 추가적인 장부 관리 없이도 멱등성(idempotent)을 유지합니다. 즉, 테이블 자체가 상태(state)가 됩니다. 50개의 후보를 처리하는 실행 과정이라도, 대부분의 쌍(pair)이 이미 캐시되어 있다면 단 3~5개의 새로운 비교 결과만 생성될 수 있습니다.
Turso (libSQL)는 세 사이트 모두를 위한 저의 데이터베이스 계층입니다. pair_slug의 UNIQUE 제약 조건은 슬러그(slug)의 안정성에 의존합니다. 지난주에 해결했던 슬러그 충돌 문제가 여기서 관련이 있는데, 두 개의 모델 슬러그로 구성된 pair_slug는 구성 요소 슬러그 중 어느 하나에서 발생하는 불안정성을 그대로 상속받기 때문입니다. 만약 모델 A의 슬러그가 실행 사이에 변경된다면, 이전의 pair 행은 고아(orphan) 데이터가 됩니다.
Astro SSG를 위한 JSON 내보내기
모든 쌍이 처리된 후, 파이프라인은 전체 model_compare 테이블을 JSON 파일로 덤프(dump)합니다:
const all = await db.execute(`SELECT * FROM model_compare ORDER BY slug_a, slug_b`);
const entries = all.rows.map((r) => ({
slug_a: String(r.slug_a),
...
Astro는 /compare/[pair_slug] 경로에 각 pair_slug에 대한 정적 페이지를 생성합니다. 현재 트래픽 수준에서는 정적 페이지를 서빙하는 비용이 전혀 들지 않고, 비교 콘텐츠가 방문자마다 달라지지 않기 때문에 SSG(Static Site Generation)를 사용하고 있습니다.
differences와 similarities 배열은 정규화된 행(normalized rows) 방식 대신 JSON 블롭(blob) 접근 방식을 사용하여 libSQL에 JSON 문자열로 저장됩니다. 수백 개의 비교 데이터 정도라면 이 방식만으로도 충분히 빠르며, 스키마를 단순하게 유지할 수 있습니다.
성공적인 부분
폴백(fallback) 템플릿 덕분에 파이프라인이 중단되지 않습니다. API 키 없이 실행하거나 예산이 소진된 상태에서도 구조적으로 유효한 페이지를 생성할 수 있습니다. CI 단계가 실패하지 않고, 단지 품질이 낮은 콘텐츠를 생성할 뿐입니다. 나중에 작동하는 키를 사용하여 파이프라인을 다시 실행할 수 있으며, 캐시 미스(cache miss) 탐지 기능이 콘텐츠 업데이트를 자동으로 처리합니다.
중복 제거(dedup) 키로서의 pair_slug는 깔끔합니다. 별도의 "이미 처리됨" 로그 파일도 없고, 데이터베이스 외부의 상태(state)도 없습니다. 행(row)이 존재하면 건너뛰고, 존재하지 않으면 생성합니다. 이는 제가 HuggingFace 모델 페치(fetch) ETL에서 사용하는 것과 동일한 패턴입니다. 즉, 데이터베이스 테이블이 곧 상태(state)입니다.
파이프라인 태그(Pipeline-tag) 그룹화는 일관된 쌍(pair)을 생성합니다. 비교 대상들이 구조적으로 주제와 관련성을 갖게 됩니다. image-to-image 모델 페이지를 방문한 사용자는 카테고리를 넘나드는 노이즈가 아니라, 다른 image-to-image 모델들과의 비교를 보게 됩니다.
내가 다르게 했을 부분
순수 다운로드 수 기반의 순위 지정은 지루한 쌍들을 선택합니다. 각 파이프라인 태그에서 다운로드 수가 가장 많은 모델들은 종종 가장 일반적인 것들입니다: bert-base-uncased, gpt2, 튜토리얼 수준의 체크포인트 등입니다. 실제 과업을 가진 개발자에게 흥미로운 비교는 종종 한 카테고리 내에서 5위와 12위에 랭크된 모델들 사이에서 이루어집니다. 이들은 실제적인 아키텍처(architectural) 차이를 가질 만큼 충분히 구체적이면서도, 커뮤니티 벤치마크가 있을 만큼 충분히 인기가 있습니다. 저는 과거의 인기에 안주하는 모델보다는 최근 탄력을 받고 있는 모델을 드러내기 위해 최신성 가중치(예: recent_30d_downloads / total_downloads)를 추가할 것입니다.
순차적 생성(Sequential generation)은 느립니다. 현재는 쌍(pair)들이 한 번에 하나씩 실행됩니다. 실행당 50개의 쌍이 있고 Haiku 호출당 약 12초가 걸린다면, 순차적 실행에는 12분이 소요됩니다. 속도 제한기(rate limiter)와 함께 510개의 동시 호출을 수행하는 비동기 배치(Async batching)를 사용하면 이를 1020초로 단축할 수 있습니다. 이 작업이 크리티컬 패스(critical path) 밖에 있는 CI 작업에서 발생하기 때문에 우선순위를 두지는 않았지만, 반복 작업(iteration)을 더 빠르게 만들어 줄 것입니다.
프롬프트 필드가 너무 개방적입니다. Claude에게 축(axis)에 대한 제약 없이 "차이점"을 나열하라고 요청하면 일관성 없는 비교 결과가 생성됩니다. 어떤 쌍은 아키텍처 차이점을, 다른 쌍은 라이선스 차이점을, 또 다른 쌍은 모호한 사용 사례(use-case) 차이점을 보여줍니다. 출력 결과가 쌍들 간에 비교 가능하지 않은 것입니다. 저는 축을 명시적으로 지정할 것입니다: "다음 항목의 차이점을 구체적으로 나열하세요: 모델 아키텍처(model architecture), 라이선스(license), 파라미터 수(parameter count), 지원 언어(supported languages), 추론 비용(inference cost)."
비교 페이지는 아직 모델 상세 페이지에서 링크되지 않습니다. Llama 3.2 페이지를 방문한 사용자는 "Llama 3.2와 비교하기:"라는 문구와 함께 캐싱된 비교 목록을 볼 수 있어야 합니다. 이를 구축하기 위한 데이터는 이미 갖춰져 있으며, UI만 연결되지 않은 상태입니다. 다음 작업으로 이를 추가할 예정입니다.
FAQ
현재 비교 페이지는 몇 개나 있나요?
아직 공개할 정확한 수치는 가지고 있지 않습니다. SELECT COUNT(*) FROM model_compare를 실행한 뒤 30일 이내에 업데이트를 게시하겠습니다. 페어링 로직(pairing logic)에 따른 이론적 상한선은 약 180개의 활성 쌍(active pairs)입니다.
Claude가 두 모델의 비교를 거부하는 경우가 있나요?
실제로는 없습니다. 프롬프트는 "개발자 중심의 비교 JSON을 생성하라"는 구체적인 기술적 작업으로 구성되어 있기 때문입니다. 가장 유사한 실패 사례는 두 모델 모두 구별 가능한 메타데이터가 거의 없는 모호한 미세 조정(fine-tune) 모델일 때, Haiku가 일반적인 차이점만을 생성하는 경우입니다. 이는 기술적으로는 유효한 출력(output)이지만, 특별히 유용하지는 않습니다.
HuggingFace에서 모델이 삭제되면 어떻게 되나요?
비교 행(row)은 데이터베이스에 그대로 남습니다. 비교 페이지는 오래된(stale) 콘텐츠를 포함한 채 계속 활성화되어 있습니다. 아직 삭제된 모델을 위한 정리 파이프라인(cleanup pipeline)은 구축하지 않았습니다. 공개 HuggingFace API는 "삭제된 모델" 피드를 제공하지 않으므로, 이를 감지하려면 야간 모델 새로고침(nightly model refresh) 중에 404 응답을 확인해야 합니다. 이는 페이지 수의 정확도에 신경을 써야 할 이유가 생기면 추가할 예정입니다.
비교할 가장 관련성 높은 모델을 선택하기 위해 임베딩 유사도(embedding similarity)를 사용하지 않는 이유는 무엇인가요?
예산과 복잡성 때문입니다. 1,500개의 모델 요약을 임베딩(embedding)하고 유사도 쿼리(similarity queries)를 실행하려면 벡터 확장 기능(Turso는 pgvector를 지원함)이나 외부 저장소가 필요합니다. 현재 트래픽이 사실상 제로인 사이트에서 그러한 인프라 오버헤드(infrastructure overhead)는 정당화되지 않습니다. pipeline_tag 그룹화 방식은 추가 인프라 비용 없이 "그럴듯하게 관련 있는 쌍(plausibly relevant pairs)"을 달성하며, 생성(generation)이나 캐싱(caching) 레이어를 변경하지 않고도 나중에 페어링 로직을 업그레이드할 수 있습니다.
- 프로덕션 디렉토리 ETL에서 Claude Haiku와 함께 사용하는 세 가지 JSON 추출 패턴 (JSON extraction patterns)
- aiappdex.com을 36시간 동안 침묵하게 만든 슬러그 충돌(slug collision)을 해결한 방법
세 개의 AI 큐레이션 디렉토리 사이트를 운영하는 6개월간의 지속적인 실험 중 일부입니다. 여기에 기술된 주장들은 사실이며, 이 글은 AI의 도움을 받아 작성되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기