AI로 장소 검색 서비스 만드는 중입니다 (기술 2) — 기능마다 다른 모델: 비용·지연·품질을 표로

기술 1편은 "로컬 LLM은 벽에 부딪혔고, 클라우드로 가되 기능마다 다른 모델을 쓰기로 했다"로 끝났습니다. 이번 글은 그 라우팅의 근거가 된 실제 숫자입니다. 그리고 여기엔 처음에 제가 갖고 있던 두 가지 착각이 나옵니다 — "AI 서비스는 부를 때마다 돈이 나간다" 와 "상위 티어 모델이 더 낫다". 둘 다 틀렸습니다. 요금은 벤더 공개가(2026-07 기준·변동 가능), 지연·품질은 유추카 자체 A/B 실측입니다.
왜 "한 모델로 통일"이 아닌가
먼저 왜 굳이 나누느냐부터. 유추카가 LLM을 부르는 지점은 크게 여섯 곳입니다.
| 기능 | 언제 | 성격 |
|---|---|---|
| 검색어 파싱 | 검색할 때마다 | 온라인·짧음·틀리면 검색 전체가 무너짐 |
| 리랭크 | 검색할 때마다 | 온라인·동기 병목 (사용자가 기다림) |
| 검색 이유 | 검색 후 | 온라인·비동기 (늦어도 화면은 안 빔) |
| 영상 장소 추출 | 채널 추가 시 | 오프라인 배치·대량 |
| 속성 채점(188개) | 신규 장소 · 야간 배치 | 오프라인 배치·초대량 |
| 속성·공간·리뷰요약 | 신규 장소 · 야간 배치 | 오프라인 배치 |
세로로 읽으면 성격이 전부 다릅니다.
파싱은 문장 한 줄을 다루는 짧은 작업이지만 여기서 틀리면 뒤가 아무리 좋아도 소용없습니다. 반대로 속성 채점은 한 곳쯤 틀려도 티가 잘 안 나지만 6천 곳을 돌려야 합니다. 리랭크는 품질보다 지연이 지배적이고, 검색 이유는 비동기라 느려도 됩니다.
여기서 통일하면 두 방향 다 손해입니다.
- 가장 좋은 모델 하나로 통일 → 6천 곳 배치에서 비용이 터집니다.
- 가장 싼 모델 하나로 통일 → 파싱에서 품질이 무너지고, 그러면 검색 전체가 무너집니다.
그래서 비용·지연·품질 세 축을 각각 재고, 기능마다 다른 모델을 붙였습니다.
축 1 — 비용
100만 토큰당 가격입니다.
| 모델 | 입력 | 출력 | 메모 |
|---|---|---|---|
| qwen-flash | $0.05 | $0.40 | 최저가 (배치 분류·요약) |
| gemini-2.5-flash-lite | $0.10 | $0.40 | 채택 (리랭크·추출·속성채점·이유) |
| gemini-3.1-flash-lite | $0.25 | $1.50 | 2.5보다 빠르나 비용 ~3배 |
| gemini-2.5-flash | $0.30 | $2.50 | 출력비 6배 · 느림 |
| qwen-plus | $0.40 | $1.20 | 배치 실험용 |
| text-embedding-v4 (임베딩) | — | — | 질의당 ~$0.000002 |
표를 볼 때 주의: 입력과 출력 가격이 다르고, 대체로 출력이 4~8배 비쌉니다. 그래서 "어떤 모델이 싼가"는 그 기능이 입력을 많이 먹는지 출력을 많이 뱉는지에 따라 답이 달라집니다. gemini-2.5-flash의 입력은 flash-lite의 3배지만 출력은 6배입니다 — 이유 생성처럼 출력이 긴 작업에서 격차가 훨씬 벌어집니다.
착각 하나 — "부를 때마다 돈이 나간다"
이 숫자를 실제 규모로 옮겨봤습니다.
검색 1회 비용: 질의 임베딩 ~$0.000002 + 리랭크(입력 몇 천 토큰, 출력 ~230 토큰). 합쳐서 1센트도 안 됩니다. 사용자가 하루에 검색을 100번 해도 커피값이 안 나옵니다.
진짜 비용은 배치였습니다.
| 작업 | 비용 |
|---|---|
| 속성 188개를 전체 6,041곳에 채점 (1회) | 169분 · 약 $31 |
| 위 작업 표본 631곳 | 약 $0.5 · 5분 |
| 코퍼스 전체 재임베딩 (1회) | ~$0.3 |
| 청크 임베딩 백필 (1회, 540만 토큰) | 수천 원 |
운영비의 대부분이 "가끔 한 번 도는 배치" 이고, 사용자 트래픽 단가는 사실상 무시할 수준이었습니다.
이게 처음 예상과 정반대였습니다. "AI 서비스는 사용자가 늘수록 돈이 무섭게 나간다"고 걱정했는데, 실제로 아팠던 건 초기 라벨링 배치 몇 번이었습니다. 그리고 배치는 한 번 하면 끝이고, 언제 돌릴지 우리가 정할 수 있고, 실패하면 다시 돌리면 됩니다.
설계에 미친 영향: 이 사실을 안 뒤로 원칙이 하나 생겼습니다. "검색할 때마다 하는 일"을 최대한 "색인할 때 한 번 하는 일"로 옮긴다. 기술 4·6편의 "무거운 LLM 판단은 색인 시점에" 원칙이 여기서 나왔습니다. 품질에도 좋고(더 큰 입력을 줄 수 있으니) 비용에도 좋고 속도에도 좋습니다.
축 2 — 지연
리랭크는 검색 응답을 붙잡는 동기 병목이라 지연이 가장 중요합니다. 두 층위로 나눠 쟀습니다.
- 순수 LLM 지연 — 엔드포인트를 직접 3회 찔러본 값
- 앱 레벨 — 파싱 + 임베딩 + 리랭크 전부 포함한 실제 응답
| 모델 | 순수 LLM (3회) | 앱 레벨 | A/B 평균 |
|---|---|---|---|
| gemini-2.5-flash-lite | ~2.0초 (2.0/2.2/2.0) | 3.2초 / 3.5초 | ~5초 |
| qwen-flash | ~2.5초 (2.5/2.8/2.2) | — | ~10초 |
| qwen-plus | — | — | ~15초 |
| gemini-3.1-flash-lite | — | — | ~3.5초 (비용 3배) |
| gemini-2.5-flash | ~9.8초 (7.8/11.8/9.8) | — | ⚠️ 편차 큼 |
왜 두 층으로 재나: 순수 LLM만 재면 "이 모델이 얼마나 빠른가"는 알 수 있지만, 우리 코드가 얼마나 느린지는 안 보입니다. 앱 레벨만 재면 그 반대고요. 둘 다 재야 "모델을 바꿀 문제"인지 "코드를 고칠 문제"인지 구분됩니다. 기술 9편에서 진범이 썸네일이었던 걸 찾아낸 것도 이 방식이었습니다.
착각 둘 — "상위 티어 모델이 더 낫다"
표에서 눈에 띄는 건 gemini-2.5-flash입니다. 이름만 보면 flash-lite의 상위 모델인데, 4~6배 느리고 편차도 큽니다.
원인은 기술 1편에서 본 그것 — thinking(추론) 모드입니다. 답하기 전에 혼자 생각을 길게 쓰고, 그 생각이 전부 출력 토큰이라 지연이 늘어납니다. 리랭크처럼 짧은 판정을 대량으로 하는 작업에서는 순수한 손해입니다.
게다가 사고가 하나 났습니다. 특정 검색어에서 추천 이유가 전부 빈 값으로 돌아왔습니다. thinking이 출력 예산을 다 써버려서 정작 답할 자리가 안 남은 것으로 보입니다. 추론 강도를 강제로 끄는 옵션을 줘야 겨우 정상화됐습니다.
정리하면 이렇습니다.
| flash-lite | flash (상위 티어) | |
|---|---|---|
| 지연 | 2.0초 | 9.8초 |
| 출력 비용 | $0.40 | $2.50 |
| 품질(P@8) | 동등 | 동등 |
| 안정성 | 항상 이유 생성 | ⚠️ 빈 값 반환 사례 |
더 비싸고, 더 느리고, 품질은 같고, 덜 안정적. 모델 티어는 추론 난도에 대한 등급이지 모든 작업에 대한 우열이 아니었습니다.
그리고 "8배 빨라졌다"는 버그였다
측정 얘기가 나온 김에 하나 고백하면, 초기에 "리랭크가 1초로, 8배 빨라졌다" 고 기록한 적이 있습니다.
알고 보니 폐기된 모델명(2.0 계열) 을 부르고 있었고, API가 404를 뱉어 LLM을 아예 안 타고 폴백하고 있었습니다. 빨라진 게 아니라 일을 안 한 거였죠. 정직한 개선은 "2배쯤"이었습니다.
교훈 둘: 개선폭이 예상 범위를 크게 넘으면 축하하기 전에 버그를 의심할 것. 그리고 모델명은 반드시 검증할 것 — 오타나 폐기된 이름을 조용히 폴백으로 삼키는 구조는 위험합니다.
축 3 — 품질, 그리고 딱 하나의 예외
리랭크 품질은 A/B 결론이 "거의 동등" 이었습니다. 상위 8건 정밀도도, 부적합으로 걸러낸 개수도 세 모델이 엇비슷했습니다.
품질이 같으면 가장 싸고 빠른 걸 고르면 됩니다. → gemini-2.5-flash-lite 채택.
(다만 qwen-flash는 추천 이유 문장이 가장 생생하고 구체적이었습니다 — "루프탑", "통창", "능선" 같은 단어를 살려 씁니다. 요금도 최저고요. 문구 품질을 더 원하면 갈아탈 대안으로 남겨뒀습니다.)
파싱만은 비싼 모델을 남겼다
전부 저렴한 모델로 내리는 흐름에서 딱 한 곳만 예외로 뒀습니다. 검색어 파싱입니다.
파싱은 검색어를 지역·업종·의도로 쪼개는 단계입니다. 여기에 flash-lite를 붙여 8개 검색어로 테스트했더니 2개가 치명적으로 깨졌습니다.
| 검색어 | 기대 | flash-lite의 해석 |
|---|---|---|
| 역삼역 10분 이내 파스타 | 지역=역삼역 / 이동시간=10분 / 메뉴=파스타 | 문장 전체가 가게 이름 |
| 스타벅스 강남 | 상호=스타벅스 / 지역=강남 | "스타벅스 강남"이 통째로 가게 이름 |
문장을 쪼개지 않고 통째로 상호명으로 읽어버립니다. 그러면 지역 조건도, 시간 조건도, 업종 조건도 전부 사라집니다. 검색은 "역삼역 10분 이내 파스타"라는 이름의 가게를 찾다가 아무것도 못 찾고 끝납니다.
8개 중 2개면 25% 실패율입니다. 다른 단계라면 감수할 수도 있는데, 파싱은 다릅니다.
파이프라인의 앞단일수록 실패 비용이 큽니다. 뒤에서 아무리 좋은 하이브리드 검색(기술 3편)과 하드게이트(기술 4편)를 돌려도, 입력이 이미 틀렸으면 전부 무의미합니다. 그리고 앞단 실패는 뒤에서 복구할 방법이 없습니다 — 잃어버린 조건은 어디에도 안 남아 있으니까요.
그래서 파싱만은 claude-haiku-4-5(상위 모델)를 유지했습니다. 실측 결론은 이랬습니다 — "경량 모델은 빠르고 싸지만 복합 구문 분해에 약하다."
여기에 하나 더 얹었습니다. 규칙 기반 사전 분류기를 앞에 둬서, 명백한 검색어("강남 카페" 같은)는 LLM을 부르기 전에 규칙으로 처리합니다. 비싼 모델을 남기되 호출 횟수를 줄이는 방향입니다. 비용과 신뢰성을 동시에 잡는 흔한 패턴이고, 부수 효과로 그 경로는 완전히 결정적이 됩니다(기술 7편에서 중요해집니다).
최종 라우팅
| 기능 | 모델 | 근거 |
|---|---|---|
| 검색어 파싱 | claude-haiku-4-5 | 신뢰성 최우선. lite는 8중 2 치명 실패 |
| 리랭크 | gemini-2.5-flash-lite | 동기 병목. ~5초 · 품질 동등 · 저렴 |
| 검색 이유 | gemini-2.5-flash-lite | 비동기 (대안: qwen-flash, 문구가 생생) |
| 속성 채점(188개) | gemini-2.5-flash-lite | 대량 배치 |
| 속성·공간·리뷰요약 | qwen-flash | 배치 · 최저가 |
| 임베딩 | text-embedding-v4 (2048차원) | 교체 시 전량 재임베딩이라 사실상 고정 |
마지막 줄에 주석이 필요합니다. 임베딩은 다른 축과 성질이 다릅니다. 리랭크 모델은 오늘 바꾸면 오늘부터 적용되지만, 임베딩을 바꾸면 저장된 벡터 전부가 무의미해집니다. 검색어는 새 모델로, 문서는 옛 모델로 만든 좌표라 같은 공간이 아니거든요. 그래서 임베딩 교체는 전체 재임베딩과 세트이고, 다른 모델 선택보다 훨씬 무겁게 결정해야 합니다.
기각한 모델도 기록해 둡니다: gemini-2.5-flash(느림·비쌈·빈 이유), gemini-2.0 계열(폐기·404), gemini-3.1-flash-lite(빠르지만 3배 비쌈), 로컬 리랭커 전부(코사인을 못 이김 — 기술 1편), exaone3.5:7.8b(형식 미준수).
갈아타기를 쉽게 만든 것
이렇게 기능마다 다른 모델을 붙이려면, 모델을 바꾸는 게 쉬워야 합니다. 안 그러면 A/B를 못 하고, A/B를 못 하면 이 표의 근거가 안 생깁니다.
문제는 로컬과 클라우드가 말하는 방식이 다르다는 점이었습니다.
| Ollama (로컬) | Gemini · Qwen (클라우드) | |
|---|---|---|
| 경로 | 네이티브 /api/chat |
OpenAI 호환 /chat/completions |
| 고유 옵션 | think: false |
— |
| 인증 | 없음 | Bearer 토큰 |
| 응답 구조 | message.content |
choices[0].message.content |
| 잘림 판정 | done_reason == "length" |
finish_reason == "length" |
그래서 클라이언트를 프로토콜을 아는 구조로 만들었습니다. 프롬프트와 JSON 파싱은 완전히 공유하고, 프로토콜에 따라 달라지는 것(경로·인증·응답 꺼내는 위치·잘림 판정)만 분기합니다. 모델을 바꾸는 일이 환경변수 네 개를 바꾸는 일이 됐습니다.
YOUCHUCA_EXTRACT_LLM_PROTOCOL=openai
YOUCHUCA_EXTRACT_LLM_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai/
YOUCHUCA_EXTRACT_LLM_MODEL=gemini-2.5-flash-lite
YOUCHUCA_EXTRACT_LLM_API_KEY=...
여기에 설정 설계를 하나 더 얹었습니다. 각 기능의 기본값이 공용 설정으로 폴백됩니다. 즉 아무것도 설정 안 하면 예전 그대로 로컬로 돌고, 원하는 기능만 환경변수를 얹으면 그 기능만 클라우드로 갑니다.
비파괴적 전환: 새 기능을 추가할 때 "설정 안 하면 예전과 완전히 동일"을 보장하면, 배포와 전환을 분리할 수 있습니다. 코드를 먼저 배포해 두고(아무 일도 안 일어남), 나중에 환경변수만 켜서 전환하고, 문제가 생기면 환경변수만 되돌립니다. 롤백에 배포가 필요 없습니다.
클라우드 경로에는 지수 백오프 재시도(3회, 500ms 시작)를 붙여 일시적인 429·503을 흡수합니다. 로컬 경로엔 안 붙였고요 — 로컬은 일시 오류가 아니라 진짜 고장이니 빨리 실패하는 게 낫습니다. (다만 기술 9편에서 보듯, 재시도는 외부 장애가 길어질 때 지연을 증폭시키기도 합니다. 공짜가 아닙니다.)
보너스 — 영상 추출: 10시간을 15분으로

전환 효과가 가장 극적이었던 건 영상에서 장소를 뽑는 추출이었습니다.
로컬 qwen3:14b |
클라우드 gemini-2.5-flash-lite |
|
|---|---|---|
| 영상당 처리 | ~45초 | 1초 미만 |
| GPU | 100% 점유 | 불필요 |
| 736영상 채널 총시간 | ~10시간 | ~15분 |
| 품질 | 기준 | 구조화 설명 추출엔 동급 이상 |
| 영상당 비용 | 전기 + 밤잠 | 1센트 미만 |
반나절 배치가 커피 한 잔 시간이 됐습니다.
여기서 놓치기 쉬운 부분이 하나 더 있습니다. 로컬 GPU는 하나뿐이라 병렬화가 불가능했습니다(기술 6편에서 병렬이 오히려 35% 느렸던 이유죠). 클라우드는 동시 호출이 자유롭습니다. 지금은 아직 직렬로 돌고 있어서, 병렬화하면 15분도 더 줄일 여지가 있습니다. 로컬에서는 애초에 존재하지 않던 선택지입니다.
그리고 이 변화가 만든 진짜 차이는 시간이 아니라 태도였습니다. 배치가 10시간이면 돌리기 전에 망설이게 됩니다. 프롬프트를 고쳐볼까 하다가도 "또 10시간"이 떠올라 그만둡니다. 15분이면 그냥 돌려봅니다. 기술 9편에서 평가 러너를 479초에서 7초로 줄인 것과 정확히 같은 효과 — 사이클이 짧아지면 시도 횟수가 늘고, 시도 횟수가 품질을 만듭니다.
이 글의 결론
- "모델을 크게"가 아니라 "기능별 적정 모델" — 파싱엔 신뢰할 모델, 리랭크엔 빠른 모델, 대량 배치엔 최저가.
- 검색 트래픽 단가는 1센트 미만이고 비싼 건 일회성 배치($31)뿐이었습니다. "부를 때마다 돈"이라는 공포는 사실과 달랐고, 이 사실이 "무거운 판단은 색인 시점에" 라는 설계 원칙을 낳았습니다.
- 상위 티어 모델이 더 낫다는 보장은 없습니다. flash는 flash-lite보다 5배 느리고 6배 비싸고 품질은 같았습니다. 모델 티어는 추론 난도의 등급이지 작업 적합도가 아닙니다.
- 파이프라인 앞단일수록 실패 비용이 큽니다. 파싱 25% 실패는 뒤 단계 전부를 무의미하게 만들기에, 여기만 비싼 모델을 남겼습니다.
- 그리고 바꾸기 쉽게 만들어 두는 것 자체가 품질 투자였습니다. 프로토콜을 추상화하고 설정을 폴백 구조로 두니, 모델 교체가 환경변수 네 줄이 됐고 그래서 A/B를 할 수 있었습니다.
다음 글(기술 3)부터는 이렇게 고른 모델들 위에서 검색 품질을 어떻게 올렸는지를 기법별로 깊게 봅니다. 먼저 "임베딩이 놓친 콩국수를 형태소로 잡은" 하이브리드 검색입니다.