ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • LLM 검증을 싸게 — haiku 1차 + sonnet 재검증 + Redis SHA 캐시
    IT 2026. 5. 29. 21:00
    LLM 검증을 싸게 — haiku 1차 + sonnet 재검증 + Redis SHA 캐시

    "LLM으로 페이지를 검증하자"는 결정은 쉽다. 그런데 막상 검증 대상이 쌓이기 시작하면 비용이 만만치 않다. 현재 deep-wiki는 22개 repo × 평균 6 페이지 = 약 132 페이지를 다루지만, 이건 시작점일 뿐이다. 대규모 OS 도메인은 모듈만 수백 개고, 모듈당 페이지도 평균 6에서 수십 페이지로 늘어난다. 개인 프로젝트로 운영하지만 다루는 콘텐츠 규모는 결코 작지 않다 — 132 페이지에서 1,000 페이지로, 다시 5,000 페이지로 비선형으로 커질 수 있다. 검증은 콘텐츠나 rubric이 바뀔 때만 일어나지만, 그 누적 호출 자체가 곧 비용이 된다. Anthropic Sonnet(고품질·고비용 모델) 단독으로만 검증한다고 가정하면 호출 1회에 $0.038. 5,000 페이지 시스템에서 rubric을 한 번 손보면 그 즉시 $190이 한꺼번에 빠지고, 페이지 업데이트가 잦아질수록 그 위로 비용이 누적된다. 작은 개인 위키 시스템이 감당할 수 있는 곡선이 아니다.

    그래서 deep-wiki는 2-tier 검증 + content-SHA 캐시를 함께 묶었다. 두 패턴의 의미를 한 줄씩 풀면 이렇다. 2-tier 검증은 같은 일을 두 단계 모델로 처리한다는 뜻 — 1차로 더 작고 빠른 Haiku(저비용 모델)를 돌리고, 점수가 임계값 미만일 때만 Sonnet에 다시 던진다(fallback). content-SHA 캐시는 페이지 본문의 해시값(SHA-256, 같은 내용엔 같은 64자 hex 문자열을 부여하는 함수)을 키로 점수를 저장해 두는 것 — 한 번 검증된 페이지는 본문이 안 바뀌는 한 다시 호출하지 않는다. 두 패턴을 함께 묶으면 호출당 평균 비용이 약 1/7로 떨어지고, 호출 수 자체도 70% 줄어든다. 이 글은 "LLM을 시스템에 박을 때 가장 먼저 박아야 하는 두 가지 패턴"의 기록이다.

    1. 배경 — 왜 검증이 필요하고 왜 비싼가

    deep-wiki는 페이지를 만들 때부터 환각이 새어 들어갈 자리를 줄이는 구조적 설계를 둔다 — 결정적으로 추출 가능한 사실(함수 시그니처·import 관계 같은)은 코드에서 직접 뽑고, 사람의 의도가 들어가는 서술은 사용자가 손으로 쓰고, LLM은 빠진 자리만 한 줄짜리 보강으로 채운다. (이 구조적 설계 자체에 대해선 별도 글에서 자세히 다룬다.)

    그러나 이 설계는 "환각이 일어날 수 있는 자리를 분리"한 것이지 "환각을 detect(탐지)"하는 단계는 아니다. LLM이 생성한 한 줄짜리 용어 정의가 정말 우리 도메인 용어에 맞는지, 사람이 손으로 쓴 prose가 코드의 실제 동작과 부합하는지는 누군가 채점해야 한다. "AI가 만든 콘텐츠를 또 다른 AI가 검증한다"는 자기 모순처럼 들리지만, 검증자와 생성자의 모델·prompt 패턴이 다르면 의외로 잘 작동한다 — 같은 모델이 자기 출력을 평가하면 자기 편향에 갇히지만, 다른 모델이 다른 관점에서 채점하면 노이즈가 잘 드러난다.

    비용은 모델별로 차이가 크다. 2026-05 기준 Anthropic 가격표는 1M 토큰당 다음과 같다:

    • Haiku 4.5: input $0.80 / output $4.00
    • Sonnet 4.6: input $3.00 / output $15.00

    한 페이지가 평균 5,000 토큰 입력 + 1,500 토큰 출력이라고 가정하면, 호출 1회당 Haiku는 약 $0.010, Sonnet은 약 $0.038. 총 비용은 (페이지 수) × (월간 평균 변경 빈도) × (호출당 비용)으로 결정된다. 즉 두 가지 방향으로 비용을 자를 수 있다 — "호출 자체를 줄인다"(콘텐츠가 바뀌지 않으면 다시 채점 안 함)와 "호출당 비용을 줄인다"(가장 싼 모델로 충분한 경우엔 비싼 모델을 안 부른다). 모든 페이지에 가장 비싼 모델을 쓰는 건 자원 낭비다 — 대부분의 페이지는 사실 잘 만들어졌고, haiku로도 충분히 그 사실을 확인할 수 있다. 그리고 한 번 검증된 페이지는 본문이 바뀌기 전까지 다시 채점할 필요가 없다.

    2. 2-tier 구조 — haiku가 거르고 sonnet이 마무리

    diagram

    흐름은 단순하다. 본문이 들어오면 가장 먼저 SHA 캐시를 조회한다. 캐시 키는 page_sha + rubric_version 두 가지의 결합이다.

    • page_sha — 페이지 본문 자체의 SHA-256 해시. 본문이 한 글자라도 바뀌면 키가 달라진다.
    • rubric_versionrubric은 채점 기준표. "가독성 0-10점, 정확성 0-10점, 다이어그램 풍부도 0-10점, ..." 식의 항목별 가중치가 적힌 JSON 파일. 그 파일이 바뀌면 version 문자열이 올라가서 키가 달라진다.

    검증에 사용한 모델 ID는 캐시 키에 포함하지 않고 메타데이터로만 저장한다. "이 점수는 claude-haiku-4-5-20251001이 매겼다"라는 사실은 함께 기록되지만, 모델이 새 버전으로 업그레이드된다고 자동 재검증하지 않는다. 이유는 단순하다 — 검증 모델이 바뀌었다고 페이지 본문이 바뀌는 건 아니기 때문이다. 모델만 바뀐 상태에서 5,000 페이지를 매번 다시 채점하면 모델 업그레이드 때마다 한 번에 $190 정도가 추가로 나간다(132 페이지 기준으로도 ~$5). 그 비용은 검증 결과를 더 좋게 만들어 주지 않는다 — 이미 통과한 페이지가 새 모델로 다시 봐도 통과할 가능성이 높기 때문. 모델 업그레이드 후 의도적으로 전체 재검증을 원하면 --revalidate-all 옵션으로 명시 트리거한다. (반면 페이지를 생성하는 모델이 바뀌면 출력된 본문이 바뀌므로 page_sha가 자동으로 달라져서 해당 페이지만 재검증된다 — 이건 콘텐츠가 바뀌었으니 자연스럽다.)

    왜 굳이 SHA를 키로 쓰나? 그냥 DB에 (repo, page_path, rubric_version) 같은 복합 키로 저장해도 되지 않나? — 사실 저장소는 DB(Redis)다. SHA는 저장 방식이 아니라 키 형태의 선택이다. 자주 액세스해서 해시가 필요한 게 아니라, content-addressable(같은 내용엔 같은 키)이라는 속성이 필요해서다. 페이지가 다른 repo로 이동하거나 경로가 바뀌어도 본문이 같으면 키가 같아 캐시가 그대로 hit한다 — repo 재구성·페이지 rename이 잦은 위키 시스템에 특히 유리하다. 또 64자 hex 문자열로 정규화되어 경로의 escape·collision 걱정이 없다. Redis의 GET/SET이 1ms 안에 끝나므로 캐시 조회 자체의 비용은 무시 가능.

    캐시 미스면 Haiku를 호출한다. 점수가 8.0 이상이면 그대로 통과(Tier A = Haiku에서 끝). 미만이면 Sonnet에 다시 던진다(Tier B = Sonnet으로 fallback). Sonnet 점수가 다시 8.0 이상이면 통과, 미만이면 hold queue(검증 실패 페이지들이 쌓이는 별도 Redis 리스트)에 들어가 텔레그램 알림이 간다 — 사람이 보거나 수정해야 한다.

    왜 Haiku가 의심하면 Sonnet으로 한 번 더 보내는가? 무엇을 얻고자 하는가

    이 구조의 핵심 질문이다. "haiku로 거른 페이지를 다시 sonnet으로 본다"는 절차에 비용이 들지 않는 건 아니다. 그래서 무엇을 얻고자 하는지가 분명해야 한다. 답은 두 가지다.

    1. Haiku의 '낮은 점수'는 '나쁘다'가 아니라 '나는 판단하기 어렵다'에 가깝다. Haiku 같은 작은 모델은 도메인 용어가 낯설거나 추상적인 prose가 들어오면 점수를 보수적으로 깎는 경향이 있다. 이때 Sonnet은 더 큰 reasoning 용량으로 같은 페이지를 다시 본다. Sonnet이 8.0 이상을 매기면 "실은 괜찮은 페이지인데 Haiku가 못 알아본 것"이라는 결론이 된다. 이게 콘텐츠 품질을 직접 끌어올리는 건 아니지만 검증 결과의 신뢰도(false positive 비율)를 끌어올린다. Haiku 단독으로 7.5점 받은 페이지를 그대로 hold queue에 넣으면 노이즈가 많고, 사람이 봤을 때 멀쩡한 페이지 비율이 높다.

    2. 사람에게 도달하는 알림의 신호 대 노이즈 비율을 높인다. Hold queue는 사람 손이 들어가는 비싼 자원이다. Haiku 점수만 믿고 모든 의심 페이지를 사람에게 보내면 일주일에 수십 건이 쌓이고, 진짜 문제 페이지가 노이즈에 묻혀 결국 아무도 안 본다. Sonnet 재검증은 사람에게 도달하는 의심 페이지를 대략 1/3로 줄였다 — 일주일 1-2건. 사람이 보는 알림이 진짜 신호가 된다.

    요약하면, 2-tier 검증의 목적은 '검증의 정확도'와 '사람 개입의 신호 대 노이즈 비'를 동시에 끌어올리는 것이다. 콘텐츠 품질 자체는 생성 단계의 구조적 설계(자료 분리, 손글 prose, LLM은 빈자리 보강)에서 결정되고, 검증은 그 결과물에 대한 채점 정확도를 다룬다.

    그리고 임계값이 두 모델 모두 8.0인 이유

    임계값은 두 모델 모두 8.0이다. 더 비싼 모델이라고 기준을 낮출 이유가 없다 — 같은 rubric에서 같은 합격선을 적용해야 비교가 단순해지고, "Tier B를 통과했다"는 사실이 곧 "Tier A 통과와 동등한 품질"을 의미하게 된다. 8.0이라는 절대값 자체는 LLM-as-judge 커뮤니티의 관행에서 가져왔다. MT-Bench(LLM-as-judge의 표준 벤치마크 논문, NeurIPS 2023)는 1-10 스케일을 사용하고, 최상위 모델들이 평균 8.99까지 받는다. 그 분포에서 8.0은 "안정적으로 좋은 응답"의 합격선으로 가장 자주 인용된다 — 7점대는 "사용 가능하지만 군데군데 문제 있음", 8점대 이상은 "사람이 따로 손볼 필요 없는 수준". deep-wiki는 후자만 자동 통과시킨다.

    두 모델의 점수 분포가 정확히 같다는 뜻은 아니다. 같은 페이지를 Haiku와 Sonnet이 약간 다르게 매길 수 있다. 하지만 "같은 rubric을 따르라"는 prompt 안에서 모델이 동일 합격선에 맞춰 채점하도록 강제하면 두 모델의 절대값 비교는 의미를 가진다. Tier A에서 7.9점이 나오면 Tier B에서 8.0 이상으로 끌어올릴 수 있는지가 관건이고, 그렇지 못하면 사람에게 넘긴다.

    3. 코드로 보면 — pinned model ID와 cost 추적

    이 패턴에서 가장 중요한 한 줄은 사실 코드가 아니라 "모델 ID를 정확하게 고정한다"는 정책이다. ~/.deep-wiki/anthropic_model_sha.txt에 줄 단위로 잠긴다. 모델이 업데이트되면 fetch_claude_model_ids.py가 이 파일을 갱신하고, 다음 호출부터 새 model_id가 쓰인다. 단 기존 캐시된 점수는 자동 무효화되지 않는다 — 모델만 바뀌고 본문은 그대로면 점수도 그대로 유효하다고 본다. 의도적 전체 재검증이 필요한 경우에만 --revalidate-all 옵션을 명시한다.

    # scripts/validate_with_claude.py — Gate 2 검증의 본체
    # claude -p를 직접 호출해 페이지를 채점한다.
    
    import hashlib
    import subprocess
    from pathlib import Path
    
    PIN_FILE = Path.home() / ".deep-wiki" / "anthropic_model_sha.txt"
    
    # 모델별 정확 단가 (Anthropic 2026-05 가격표)
    # Haiku는 sonnet의 약 1/4 가격 — 1차 게이트로 충분
    PRICE = {
        "haiku": (0.80 / 1_000_000, 4.00 / 1_000_000),     # in / out
        "sonnet": (3.00 / 1_000_000, 15.00 / 1_000_000),
    }
    
    # 임계값은 두 모델 모두 8.0 — MT-Bench 관행(8점대 이상 = '사람이
    # 손볼 필요 없는 수준')을 따라 통일된 합격선을 둔다.
    GATE_2A_PASS = 8.0   # Haiku 통과 임계
    GATE_2B_PASS = 8.0   # Sonnet 최종 합격선 (동일)
    
    
    def cache_key(page_body: str, rubric_version: str) -> str:
        """캐시 키 = SHA256(page_body) + rubric_version.
    
        모델 ID는 키에 포함하지 않는다 — 검증 모델이 바뀐다고 본문이 바뀌는
        건 아니므로 기존 점수를 무효화하지 않는다. 모델은 점수와 함께
        metadata로 저장될 뿐. 의도적 전체 재검증은 --revalidate-all로 분리.
        """
        page_sha = hashlib.sha256(page_body.encode("utf-8")).hexdigest()
        return f"validation:{page_sha}:{rubric_version}"
    
    
    def load_pins() -> dict[str, str]:
        """anthropic_model_sha.txt에서 정확한 모델 ID를 읽는다.
    
        예시 내용:
            haiku=claude-haiku-4-5-20251001
            sonnet=claude-sonnet-4-6
        """
        pins = {}
        for line in PIN_FILE.read_text().splitlines():
            if "=" in line and not line.startswith("#"):
                role, model = line.split("=", 1)
                pins[role.strip()] = model.strip()
        return pins
    
    
    def call_claude(model_id: str, prompt: str, timeout: int = 90) -> tuple[str, int]:
        """claude CLI를 subprocess로 호출. 정확한 model_id를 명시."""
        try:
            result = subprocess.run(
                ["claude", "--model", model_id, "-p", prompt],
                capture_output=True, text=True,
                timeout=timeout, check=False,
            )
        except subprocess.TimeoutExpired:
            return "", 124
        return result.stdout, result.returncode
    
    
    def estimate_cost(prompt_chars: int, output_chars: int, tier: str) -> float:
        """대략적 토큰 단가 계산. 점수와 함께 Redis에 누적."""
        in_tok = prompt_chars / 3.5    # ko/en 혼합 평균
        out_tok = output_chars / 3.5
        p_in, p_out = PRICE[tier]
        return in_tok * p_in + out_tok * p_out
    

    위 코드는 두 가지를 함께 한다. 첫째, cache_key()는 본문 SHA + rubric version만으로 키를 만들어 "본문이 그대로면 모델이 바뀌어도 점수 재사용"을 보장한다. 둘째, 모델 ID를 하드코딩하지 않고 load_pins()가 외부 파일에서 읽어, 호출 시점에 정확한 model_id를 명시한다. 비용은 매 호출마다 누적되어 cost_monitor.py가 매일 정리해 dashboard에 띄운다.

    # scripts/cost_monitor.py — 누적 비용 + 캐시 hit rate 일일 리포트
    
    THRESHOLDS = (50.0, 80.0, 100.0)   # 월 임계 — 넘으면 텔레그램 알림
    
    
    def summary() -> dict:
        client = content_sha_cache._client()    # Redis 클라이언트
        today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
        total = client.get("deep-wiki:cost:total")
        today_cost = client.get(f"deep-wiki:cost:{today}")
        stats = content_sha_cache.stats()
        hit_rate = stats["hits"] / (stats["hits"] + stats["misses"]) if accesses else 0
        return {
            "total_usd": float(total or 0),
            "today_usd": float(today_cost or 0),
            "cache_hit_rate": round(hit_rate, 4),
            "monthly_threshold_usd": 100.0,
            "alert_thresholds_usd": list(THRESHOLDS),
        }
    

    매일 02:30 cron이 한 번 돌고 나면 cost_monitor.py가 누적치를 갱신한다. $50을 넘으면 yellow, $80이면 orange, $100이면 red 텔레그램 알림이 간다. 비용이 폭주하기 전에 시스템이 자신을 알린다.

    4. 캐시 hit rate — 실전 수치

    diagram

    실제 운영에서 캐시 hit rate는 약 70%로 안착했다. 검증 자체는 콘텐츠나 rubric이 바뀔 때만 트리거되지만, 그 호출 흐름 안에서도 같은 본문을 다시 채점하는 일이 의외로 자주 일어난다 — 페이지가 빌드 파이프라인을 통과할 때마다, rubric 튜닝 후 회귀 확인 때마다, 모델 핀이 갱신될 때마다 동일 본문에 검증 호출이 다시 떨어진다. 캐시는 그 중복 호출들을 흡수한다. 한 페이지에 대한 첫 검증은 정상 호출로 빠지고, 그 뒤 본문이 안 바뀌어 있는 동안의 모든 추가 호출은 0 비용으로 끝난다.

    위 비교에서 핵심은 단지 호출당 $0.005 vs $0.038이 아니라, "호출당 비용 × 호출 수"의 두 축을 동시에 누른다는 점이다. 2-tier 구조는 호출 1회당 비용을 깎고, content-SHA 캐시는 호출 수 자체를 70% 깎는다. 총 비용은 두 효과의 곱이다 — 그래서 페이지 수가 5,000개로 늘어도 비용 곡선이 폭주하지 않고 천천히 따라간다. 5,000 페이지 시스템에서 rubric을 한 번 손보는 사건은 A 시나리오에선 $190 즉시 청구이지만, C 시나리오에선 ≈ $25에 머문다. 검증 품질을 깎지 않고도 스케일 자유도를 확보한다는 의미다.

    5. 거절된 대안 — 더 단순한 길은 없었나

    • 모든 페이지를 sonnet으로 — 가장 안전하지만 호출당 $0.038로 가장 비싸다. 페이지 수와 변경 빈도에 그대로 비례해서 누적되고, 절반 이상의 호출이 사실상 동일한 점수 반복이라 비용 대비 가치도 음수.
    • 모든 페이지를 haiku로 — 싸지만 노이즈 페이지를 못 잡는다. Haiku가 score 7.5를 매긴 페이지가 실은 진짜 문제인지 Haiku의 판단 한계인지 확인하려면 어차피 sonnet이 필요하다.
    • 모델 ID도 캐시 키에 포함 — 직관적이지만 검증 모델이 새 버전으로 갈 때마다 모든 페이지가 자동 재검증되어 비용 spike. 검증 모델이 바뀐다고 콘텐츠가 바뀐 건 아니므로 자동 트리거할 이유가 없다. 모델은 점수와 함께 metadata로만 기록하고, 의도적 재검증은 명시 옵션으로 분리.
    • 관계형 DB에 (repo, page_path, model, rubric, score) 행으로 저장 — 작동은 하지만 페이지가 다른 repo로 이동·rename될 때 캐시 일관성이 깨진다. 자연 키는 경로에 의존하기 때문이다. SHA는 content-addressable이라 본문 동일성만으로 hit. 위키 시스템처럼 페이지 위치가 자주 재구성되는 환경에서 특히 유리.
    • 로컬 gemma로 검증 — 검증자와 생성자가 같은 모델이면 자기 검증 함정. claude API는 외부 라인이라 prompt·trainset이 달라 노이즈를 효과적으로 잡는다.
    • Anthropic의 prompt cache 기능 사용 안 함 — Anthropic API는 prompt cache라는 자체 기능을 제공한다(같은 prompt 앞부분을 재사용하면 그 부분 토큰 비용을 90% 깎아 줌). 그런데 우리 패턴에서는 prompt가 매 호출마다 다르고(페이지 내용 자체가 prompt 본문) 캐시 효과가 적어 굳이 enable 안 했다. content SHA 캐시가 훨씬 강력 — 아예 호출 자체를 안 한다.

    6. 운영 결과 — 비용 평탄화 + 사람 개입 최소화

    이 패턴이 정착한 직후 두 가지가 동시에 좋아졌다. 첫째, 월 비용이 예측 가능해졌다. 70% 캐시 hit + 80% haiku만 통과 + 20% sonnet fallback이라는 비율이 안정적으로 유지되어, rubric 갱신처럼 검증 호출이 한꺼번에 몰리는 사건에도 비용이 예측 범위 안에 머물렀다. 임계 알림($50/$80/$100)이 사실상 한 번도 발동되지 않았다.

    둘째, 사람 개입이 자동으로 최소화됐다. Tier B(sonnet)에서도 score 8.0 미만으로 떨어진 페이지만 hold queue에 들어가 텔레그램으로 알림이 왔다. 실제로 알림은 일주일에 1-2건 수준이었고, 대부분은 docs/architecture.md를 손봐야 하는 진짜 문제인 경우였다. "신뢰할 수 없는 페이지"라는 작은 집합만 사람에게 도달한다.

    이 패턴의 부수 효과 하나 — 모델 업데이트가 비용을 폭주시키지 않는다. Anthropic이 Haiku를 새 버전으로 올리면 fetch_claude_model_ids.py가 핀 파일을 갱신하고, 다음 호출부터 새 model_id가 자동 적용된다. 단, 기존 캐시는 자동 무효화되지 않는다 — 본문이 바뀌지 않은 페이지는 이전 점수를 그대로 유지한다. 이미 검증된 콘텐츠를 새 모델이 다시 봐도 결과가 크게 달라지지 않을 가능성이 높기 때문이다. 모델 메이저 변경 후 신뢰성 확인이 필요하면 --revalidate-all로 의도적으로 트리거하고, 그렇지 않으면 본문 변경분만 자연스럽게 새 모델로 채점된다. ~/.deep-wiki/anthropic_model_sha.txt가 시스템 전체의 모델 truth source라서 어디서도 stale model_id를 들고 다니지 않는다는 점은 동일하게 유지된다.

    마무리 — LLM을 시스템에 박을 때 가장 먼저 박을 두 가지

    LLM 비용은 "중복 호출이 많은 시스템"에서 빠르게 누적된다. 콘텐츠가 바뀌지 않았는데 같은 페이지에 검증 호출이 반복적으로 떨어지고, 의심스럽지 않은 페이지에까지 비싼 모델이 호출되면 그 둘이 곱셈으로 비용을 만든다. 그 곱셈을 자르는 두 가지 패턴이 있다.

    첫째, 해야 하는 호출만 해라. Content SHA + rubric version 캐시가 "이건 이미 답을 안다"를 정확히 식별한다. 한 번 검증된 페이지에 두 번째 검증을 던지지 않고, 본문이 바뀌어야만 다시 본다. 둘째, 가장 싼 모델을 먼저 시도하고, 의심스러울 때만 비싼 모델로. 모델은 동질하지 않다. 더 비싼 모델이 모든 영역에서 더 정확한 것도 아니다. 비싼 모델은 의심스러운 입력에만 진가를 발휘한다.

    이 두 가지를 함께 잠그면 호출당 비용을 1/7로 깎고, 호출 수도 70% 줄여서 같은 품질의 검증을 1/20에 가까운 비용으로 받는다. AI Architect가 LLM을 시스템에 처음 박을 때 가장 먼저 박아야 하는 두 가지가 바로 이것이다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.