Published on

Self-Consistency로 CoT 정확도↑ 비용↓ 실전

Authors

서로 다른 추론 경로를 여러 번 생성해 가장 일관된 답을 선택하는 Self-Consistency는, CoT 기반 문제에서 정확도를 유의미하게 올리는 대표 기법입니다. 하지만 단순히 n회 호출을 늘리면 비용이 폭증합니다. 이 글에서는 정확도 상승을 얻으면서도 비용을 낮추는(또는 증가폭을 최소화하는) 실전 패턴을 정리합니다.

또한 CoT를 그대로 노출하지 않고 품질을 올리는 방향(감사·보안·제품 정책)까지 함께 다룹니다. 관련해서는 CoT 유출 없이 추론 품질 올리는 5가지 기법도 같이 보면 설계 선택지가 넓어집니다.

Self-Consistency란 무엇이고 왜 통하는가

일반적인 CoT 프롬프트는 한 번의 샘플링에서 하나의 추론 경로만 얻습니다. 모델은 확률적으로 토큰을 생성하므로, 같은 문제라도 샘플링에 따라 다른 경로를 만들 수 있고 그중 일부는 우연히 오류를 포함합니다.

Self-Consistency는 다음 아이디어를 사용합니다.

  • 동일 문제를 여러 번 샘플링하여 서로 다른 추론 경로(또는 최종 답)를 모은다
  • 최종 답을 기준으로 집계(다수결, 가중치 투표 등)한다
  • 가장 많이 나온 답을 채택한다

핵심은 “추론 경로의 다양성”과 “정답의 수렴”입니다. 어려운 문제일수록 단일 경로의 오류 확률이 커지고, 여러 경로를 모아 집계했을 때 오류가 상쇄될 가능성이 커집니다.

비용이 왜 늘고, 어디서 줄일 수 있나

Self-Consistency의 가장 큰 단점은 호출 수 증가입니다.

  • 호출 비용 증가: n번 호출하면 기본적으로 비용이 n
  • 지연 증가: 병렬화하지 않으면 레이턴시도 n
  • 운영 복잡도 증가: 타임아웃, 재시도, 로깅, 캐시, 품질 모니터링 등

그럼에도 “비용↓”을 이야기할 수 있는 이유는 다음과 같습니다.

  1. 재시도 비용을 줄인다: 단일 호출로 틀리면 재질문/재시도/사람 검수로 이어지는 비용이 큼
  2. 고비용 모델 호출을 줄인다: 저비용 모델로 여러 번 뽑아 집계하고, 애매할 때만 상위 모델로 승급
  3. 토큰을 줄인다: CoT 전체를 길게 생성하지 않고 “짧은 추론” 또는 “최종 답만”을 여러 번 뽑아도 효과가 나는 케이스가 있음

즉, “한 번에 비싸게 정확” vs “여러 번 싸게 수렴” vs “혼합 전략” 중에서 워크로드에 맞는 최적점을 찾는 문제입니다.

기본 구현: 다수결 집계(최종 답 기준)

Self-Consistency의 실전 구현은 의외로 단순합니다. 중요한 것은 집계 단위를 무엇으로 삼느냐입니다.

  • 산술/객관식: 최종 답을 정규화하여 동일성 비교
  • 서술형: 핵심 주장/엔티티를 추출하여 비교하거나, 별도 심판 모델로 동치성 판정

아래는 Python으로 “최종 답만”을 여러 번 생성하고 다수결로 고르는 예시입니다.

from collections import Counter
import re

def normalize_answer(text: str) -> str:
    # 예: 숫자/선택지/짧은 답 정규화
    t = text.strip().lower()
    t = re.sub(r"\s+", " ", t)
    # 모델이 "정답: X" 같은 포맷을 섞는 경우 제거
    t = re.sub(r"^answer\s*:\s*", "", t)
    return t

def majority_vote(answers):
    norm = [normalize_answer(a) for a in answers]
    c = Counter(norm)
    winner, count = c.most_common(1)[0]
    return winner, count, c

# pseudo: call_llm(prompt, temperature=...)
# answers = [call_llm(prompt, temperature=0.8) for _ in range(n)]
# final, count, dist = majority_vote(answers)

여기서 정확도를 올리는 포인트는 temperature입니다.

  • 너무 낮으면(예: 0.0) 샘플이 비슷해져서 Self-Consistency 이점이 줄어듦
  • 너무 높으면 엉뚱한 답이 늘어 집계가 흔들릴 수 있음

실무에서는 0.5 전후에서 시작해, 분포를 보고 조정하는 경우가 많습니다.

비용을 낮추는 핵심: “언제 n을 늘릴지”를 결정하라

모든 요청에 n=10을 적용하면 비용은 선형으로 증가합니다. 실전에서는 적응형(Adaptive) Self-Consistency가 거의 필수입니다.

1) 조기 종료(Early stop) 규칙

예를 들어 n을 최대 7로 두고, 3번 샘플링만에 동일 답이 3번 나오면 종료하는 방식입니다.

  • 쉬운 문제: 3회 내 수렴, 비용 절약
  • 어려운 문제: 끝까지 샘플링, 정확도 확보
def self_consistency(prompt, call_llm, max_n=7, min_n=3, win_threshold=3, temperature=0.7):
    answers = []
    counts = Counter()

    for i in range(max_n):
        a = call_llm(prompt, temperature=temperature)
        na = normalize_answer(a)
        answers.append(na)
        counts[na] += 1

        # 최소 샘플 수를 채운 뒤 조기 종료
        if i + 1 >= min_n:
            winner, cnt = counts.most_common(1)[0]
            if cnt >= win_threshold:
                return {
                    "answer": winner,
                    "samples": i + 1,
                    "distribution": dict(counts),
                    "early_stop": True,
                }

    winner, cnt = counts.most_common(1)[0]
    return {
        "answer": winner,
        "samples": max_n,
        "distribution": dict(counts),
        "early_stop": False,
    }

win_threshold는 워크로드에 맞게 튜닝합니다.

  • 객관식/단답: 3-of-5, 4-of-7처럼 강하게
  • 노이즈 큰 자연어: 너무 강하면 끝까지 못 수렴하므로 “상대 우위” 규칙도 병행

2) 불확실성 기반 승급(Escalation)

저비용 모델로 Self-Consistency를 먼저 돌리고, 결과가 애매하면 상위 모델로 승급합니다.

애매함의 신호 예시:

  • 최빈값 비율이 낮다(예: 7개 중 최빈 3개)
  • 1, 2위가 비슷하다
  • 정규화 후에도 답이 여러 형태로 갈라진다

이 패턴은 에이전트에서도 특히 유용합니다. 무의미한 툴 호출이 늘어나는 상황을 막는 가드레일과 함께 쓰면 비용이 급격히 안정화됩니다. 관련해서는 LangChain 에이전트 무한루프·툴폭주 차단법을 참고할 만합니다.

CoT를 길게 쓰지 않고도 Self-Consistency를 쓰는 법

Self-Consistency는 “여러 경로의 추론”이 본질이지만, 반드시 화면에 긴 CoT를 출력할 필요는 없습니다.

실전에서 많이 쓰는 방식은 다음 중 하나입니다.

  1. 내부적으로는 추론하되 출력은 최종 답만
  2. 짧은 근거 요약만 출력
  3. 검증 가능한 중간 산출물만 출력(예: 수식, 테이블, 체크리스트)

프롬프트 예:

문제를 풀되, 최종 답만 출력하세요.
추론 과정은 내부적으로만 수행하세요.
출력 형식: Answer: ...

이렇게 하면 토큰이 줄고, 정책적으로 CoT 노출을 피할 수 있으며, Self-Consistency에 필요한 것은 “다양한 샘플”이므로 여전히 효과를 볼 수 있습니다.

다만 모델/제공자에 따라 “내부 추론 숨김”이 실제로 토큰을 줄이는지 여부는 다를 수 있습니다. 따라서 과금 토큰(입력/출력)과 응답 길이를 관측해 확인해야 합니다.

집계 품질을 올리는 정규화·스코어링 테크닉

Self-Consistency의 실패 원인은 “모델이 틀렸다”가 아니라 “집계가 허술했다”인 경우도 많습니다.

1) 답 정규화(Answer normalization)

  • 숫자 표기: 1,000 vs 1000
  • 단위: 10 ms vs 0.01 s
  • 선택지: A vs 1 vs

가능하면 문제 유형별로 정규화 함수를 분리하세요.

2) 가중치 투표(Weighted voting)

각 샘플에 대해 모델이 함께 내는 신호를 가중치로 활용할 수 있습니다.

  • 자체 신뢰도 점수(모델이 숫자로 출력)
  • 검증기(checker)의 통과 여부
  • 규칙 기반 검증(예: 수식 재계산)

예: “답과 함께 신뢰도 0.0부터 1.0을 출력”하게 한 뒤 가중 합으로 선택.

import json
from collections import defaultdict

def weighted_vote(json_outputs):
    scores = defaultdict(float)
    for s in json_outputs:
        obj = json.loads(s)
        ans = normalize_answer(obj["answer"])
        conf = float(obj.get("confidence", 0.5))
        scores[ans] += conf
    winner = max(scores.items(), key=lambda x: x[1])[0]
    return winner, dict(scores)

주의할 점은 “모델의 자기평가”가 항상 믿을 만하지 않다는 것입니다. 그래서 실무에서는 검증기 기반 가중치가 더 안정적입니다.

Self-Consistency와 검증(Verifier)을 결합하면 더 싸질 수 있다

가장 강력한 패턴은 다음 2단계입니다.

  1. 생성기(Generator): 다양한 답 후보를 만든다(Self-Consistency 샘플)
  2. 검증기(Verifier): 후보를 빠르게 검증한다(규칙, 실행, 다른 모델)

예를 들어 산술/로직/코드 문제는 검증이 싸게 가능합니다.

  • 수학: 파이썬으로 재계산
  • SQL: 샌드박스에서 쿼리 실행
  • 코드: 단위 테스트 실행

이 경우 n을 늘려도 “정답 판별 비용”이 낮아 전체 비용이 안정됩니다.

운영 관점: 지연, 병렬화, 캐시, 관측성

1) 병렬 호출로 레이턴시를 상수로 만들기

Self-Consistency는 n회 호출이므로, 가능하면 병렬로 날려 레이턴시를 줄입니다.

  • 서버리스/비동기 런타임: 동시성 제한 관리
  • 레이트 리밋: 백오프 및 큐잉

2) 캐시 전략

  • 동일 입력 프롬프트는 캐시로 비용 절감
  • 다만 temperature가 높으면 캐시 효율이 떨어지므로, “최종 답만 캐시”하거나 “문제 해시 기반 캐시”를 고려

3) 관측성 지표

Self-Consistency는 “정답률”만 보면 튜닝이 어렵습니다. 아래를 같이 봐야 합니다.

  • 평균 샘플 수(조기 종료가 잘 되는지)
  • 분포 엔트로피(답이 얼마나 갈리는지)
  • 최빈값 비율(수렴도)
  • 승급률(상위 모델로 얼마나 넘어가는지)
  • 토큰/요청당 비용, p95 레이턴시

실전 튜닝 레시피: 추천 기본값

워크로드마다 다르지만, 시작점으로 쓸 만한 조합은 다음과 같습니다.

  • 쉬운 단답/객관식

    • temperature=0.6
    • max_n=5, min_n=3, win_threshold=3
    • 최빈값이 3-of-3이면 즉시 종료
  • 중간 난이도 서술형(정규화 어려움)

    • 답을 JSON으로 강제하고 핵심 필드만 비교
    • max_n=7, min_n=3, win_threshold=3
    • 최빈값 비율이 낮으면 검증기 또는 상위 모델 승급
  • 에이전트/툴 사용

    • 먼저 “툴 없이 답 가능한지” 분기
    • 툴 사용 시에는 호출 예산과 스텝 제한을 강제
    • Self-Consistency는 “최종 결론”에만 적용하거나, “계획 단계”와 “실행 단계”를 분리

Next.js 제품에 붙일 때 주의할 점

Self-Consistency는 서버 측에서 여러 번 호출하므로, 프론트 성능에도 영향을 줍니다.

  • 스트리밍 UI: 중간 결과를 보여주되 최종 확정은 집계 후
  • 타임아웃 UX: 일정 시간 내 수렴 실패 시 “추가 계산 중” 상태 표시
  • 캐시/프리페치: 자주 묻는 질문은 서버 캐시

프론트 지표(INP 등)까지 함께 최적화할 때는 React/Next.js 최적화로 INP 200ms 줄이기 같은 접근과 같이 보는 것이 좋습니다.

흔한 실패 패턴과 해결

1) 답이 다양한데 다수결이 의미가 없다

원인:

  • 문제 자체가 모호함
  • 출력 포맷이 자유로워 정규화가 실패

해결:

  • 출력 스키마를 강제(JSON)
  • 답을 “선택지”로 변환(모델에게 보기 생성 후 선택)
  • 동치성 판정용 심판 모델을 추가

2) 비용이 줄지 않고 늘기만 한다

원인:

  • 모든 요청에 고정 n
  • 조기 종료/승급이 없음
  • 출력이 길어 토큰이 커짐

해결:

  • 적응형 n + 조기 종료
  • 최종 답만 출력
  • 저비용 모델 우선, 애매할 때만 승급

3) Self-Consistency가 오히려 틀린 답으로 수렴한다

원인:

  • 모델이 특정 오답 패턴에 강하게 끌림(편향)
  • 프롬프트가 오답을 유도

해결:

  • 샘플 다양성 증가: temperature 조정, 프롬프트 변형(paraphrase)
  • 검증기 결합(규칙/실행)
  • “반례 찾기” 프롬프트를 추가해 후보를 공격적으로 검증

마무리: 정확도와 비용의 균형점은 설계로 만든다

Self-Consistency는 “정확도를 돈으로 사는 기법”처럼 보이지만, 실전에서는 적응형 샘플링, 조기 종료, 승급, 검증기 결합, 출력 토큰 최소화를 통해 비용을 통제할 수 있습니다.

정리하면 다음 순서로 적용하는 것을 권합니다.

  1. 최종 답 중심 집계 + 정규화부터 구축
  2. 조기 종료로 평균 n을 낮춤
  3. 애매한 케이스만 상위 모델 승급
  4. 가능한 문제는 검증기로 정답 판별 비용을 낮춤
  5. CoT는 노출하지 않고도 품질을 올리는 방향으로 운영

이 조합이 갖춰지면, CoT 단발 호출 대비 “정확도는 올라가고, 재시도/검수까지 포함한 총비용은 내려가는” 구간을 만들 수 있습니다.