Published on

CoT 없이도 정확도↑ - Self-Consistency 구현

Authors

서로 다른 샘플을 여러 번 뽑아 다수결로 답을 고르는 Self-Consistency는, 모델의 추론 과정을 길게 출력하는 CoT 없이도 정확도를 올릴 수 있는 실전적인 기법입니다. 특히 제품 환경에서는 CoT를 그대로 노출하면 보안/정책/UX 측면에서 부담이 커지는데, Self-Consistency는 출력은 짧게 유지하면서도 샘플링 기반의 앙상블 효과로 오답을 줄일 수 있습니다.

이 글에서는 “CoT를 사용자에게 보여주지 않는다”는 전제에서, Self-Consistency를 어떻게 설계하고 구현하며, 어떤 지표로 튜닝해야 하는지까지 한 번에 정리합니다.

Self-Consistency란 무엇인가

Self-Consistency는 간단히 말해 다음 절차입니다.

  1. 같은 질문을 n번 반복 호출한다
  2. 매번 샘플링 다양성을 주기 위해 temperature를 올리거나 top_p를 조정한다
  3. 나온 답들을 정규화한 뒤
  4. 다수결(majority vote) 또는 가중 투표로 최종 답을 선택한다

CoT를 “출력”하지 않아도 되는 이유는, Self-Consistency의 핵심이 여러 경로의 추론을 내부적으로 시도하고 그 결과의 합의(consensus)를 취하는 데 있기 때문입니다. 즉, 사용자는 최종 답만 받아도 됩니다.

언제 효과가 큰가

  • 수학/논리/규칙 기반 문제처럼 정답이 명확한 태스크
  • 툴 호출 없이 텍스트만으로 해결 가능한 문제
  • 모델이 한 번의 샘플링에서 흔들리는(variance가 큰) 문제

반대로, 정답이 애매하거나(에세이/창작), 답이 길고 다양한 형태로 나오는 문제는 “정규화”가 어려워 투표가 잘 안 먹힐 수 있습니다.

CoT 없이 Self-Consistency를 쓰는 프롬프트 패턴

핵심은 모델이 길게 설명하지 않게 강제하고, 최종 답을 구조화해서 투표 가능한 형태로 만드는 것입니다.

다음 두 가지가 실무에서 가장 안정적입니다.

  1. 최종 답만 출력 강제
  2. JSON 스키마로 출력 형태 고정

예시 프롬프트(개념):

  • “설명하지 말고 최종 답만”
  • “출력은 JSON만”
  • answer 필드에만 답을 넣어라”

이렇게 하면 CoT를 노출하지 않으면서도, 여러 샘플을 비교할 수 있는 동일한 인터페이스를 확보합니다.

구현 아키텍처: 샘플링·정규화·투표·검증

Self-Consistency를 제품에 넣을 때는 단순히 n번 호출하고 다수결로 끝내면 장애/비용/지연에서 바로 벽을 만납니다. 아래 4단계로 나눠 설계하면 운영이 쉬워집니다.

1) 샘플링 전략

  • n: 3~9 범위에서 시작
  • temperature: 0.7~1.0에서 다양성 확보
  • top_p: 0.9 전후
  • max_tokens: 답만 내게 해서 짧게

: 비용과 지연이 부담이면 early stop을 넣습니다. 예를 들어 5번까지 뽑되, 3번 연속 같은 답이 나오면 즉시 종료합니다.

2) 정규화(normalization)

투표 전에 답을 “비교 가능한 키”로 바꿔야 합니다.

  • 공백/개행 제거
  • 대소문자 통일
  • 숫자 표현 통일(예: "1,000""1000"으로)
  • 단위/기호 정리

정규화가 약하면 같은 답이 다른 문자열로 취급되어 투표가 깨집니다.

3) 집계(majority vote 또는 weighted vote)

가장 단순한 것은 빈도수 최댓값을 고르는 것입니다. 하지만 실무에서는 다음을 섞으면 더 안정적입니다.

  • 빈도수
  • 모델이 준 confidence(가능하면)
  • 답 검증기(verifier)의 점수

4) 검증(verifier) 또는 제약(constraint)

Self-Consistency는 “다수결”이므로, 다수가 틀리면 그대로 틀립니다. 그래서 마지막에 가벼운 검증 레이어를 두면 좋습니다.

  • 정답 형식 검증(정규식)
  • 단위/범위 체크
  • 외부 툴로 계산 검증(가능하면)

운영에서 이 검증 레이어는 guardrail 역할을 합니다. 장애 대응 관점은 쿠버네티스처럼 원인별로 빠르게 진단하는 체계가 중요합니다. 예를 들어 모델 호출 실패, 타임아웃, 레이트리밋 등은 재시도 정책과 함께 로그를 남겨야 합니다. 장애 유형별 접근은 Kubernetes CrashLoopBackOff 원인별 로그·해결 9가지처럼 “원인 분류”가 핵심입니다.

Node.js로 Self-Consistency 구현 예제

아래 예시는 OpenAI 호환 API 스타일을 가정한 의사 코드입니다. 포인트는 다음입니다.

  • n번 호출
  • 답을 JSON으로 강제
  • 파싱 실패는 버리고 계속
  • 정규화 후 투표
  • 동률이면 추가 샘플링 또는 보수적 fallback
import crypto from "node:crypto";

type Sample = {
  raw: string;
  answer: string;
};

function normalizeAnswer(s: string) {
  return s
    .trim()
    .toLowerCase()
    .replaceAll(/\s+/g, " ")
    .replaceAll(",", "");
}

function majorityVote(samples: Sample[]) {
  const counts = new Map<string, number>();
  const firstSeen = new Map<string, Sample>();

  for (const s of samples) {
    const key = normalizeAnswer(s.answer);
    counts.set(key, (counts.get(key) ?? 0) + 1);
    if (!firstSeen.has(key)) firstSeen.set(key, s);
  }

  let bestKey: string | null = null;
  let bestCount = -1;
  for (const [k, c] of counts.entries()) {
    if (c > bestCount) {
      bestCount = c;
      bestKey = k;
    }
  }

  if (!bestKey) return null;
  return {
    key: bestKey,
    count: bestCount,
    sample: firstSeen.get(bestKey)!,
  };
}

async function callLLM(prompt: string, temperature: number) {
  // 실제 구현에서는 fetch로 호출
  // 아래는 응답 형태만 가정
  return {
    content: "{\"answer\": \"42\"}",
  };
}

async function selfConsistencyAnswer(question: string) {
  const n = 7;
  const basePrompt = [
    "You are a precise assistant.",
    "Do not explain.",
    "Return only JSON.",
    "Schema: {\"answer\": string}",
    `Question: ${question}`,
  ].join("\n");

  const samples: Sample[] = [];

  for (let i = 0; i < n; i++) {
    const temperature = 0.8 + i * 0.05;
    const res = await callLLM(basePrompt, temperature);

    try {
      const obj = JSON.parse(res.content);
      if (typeof obj.answer !== "string") continue;
      samples.push({ raw: res.content, answer: obj.answer });

      // early stop: 최근 3개가 모두 같은 답이면 종료
      if (samples.length >= 3) {
        const a = normalizeAnswer(samples[samples.length - 1].answer);
        const b = normalizeAnswer(samples[samples.length - 2].answer);
        const c = normalizeAnswer(samples[samples.length - 3].answer);
        if (a === b && b === c) break;
      }
    } catch {
      // JSON 파싱 실패는 폐기
      continue;
    }
  }

  const voted = majorityVote(samples);
  if (!voted) {
    return {
      answer: null,
      reason: "no-valid-samples",
    };
  }

  return {
    answer: voted.sample.answer,
    consensus: voted.count / samples.length,
    samples: samples.length,
  };
}

(async () => {
  const result = await selfConsistencyAnswer("What is 6 * 7?");
  console.log(result);
})();

구현 디테일: 요청 상관관계와 로그

Self-Consistency는 호출이 여러 번 발생하므로, 운영에서 트레이싱이 필수입니다.

  • request_id를 생성하고 모든 샘플 호출에 전달
  • 각 샘플의 temperature, 응답 시간, 파싱 성공 여부를 로그로 남김

Node.js 환경에서는 런타임 설정에 따라 경로/모듈 처리에서 자주 삐끗합니다. ESM 기반 코드에서 로깅/파일 저장을 하다 __dirname 이슈를 만나면 디버깅 시간이 늘어날 수 있으니, 관련 정리는 Node.js ESM에서 __dirname 미정의 해결 5가지를 함께 참고해두면 좋습니다.

비용·지연 최적화: 무작정 n을 늘리면 망한다

Self-Consistency의 비용은 거의 선형으로 증가합니다.

  • 비용: O(n)
  • 지연: 병렬이면 max(latency)에 가깝지만, 레이트리밋/큐잉 때문에 실제로는 더 늘어남

실무 최적화 체크리스트:

  1. 병렬 호출: 가능하면 병렬로 쏘고, 타임아웃 짧게
  2. early stop: 합의가 빨리 나오면 중단
  3. 2단계 전략: 기본은 3샘플, 불확실할 때만 7샘플
  4. 캐시: 동일 질문은 해시 키로 캐시
  5. 검증기 우선: 형식 검증으로 폐기 샘플을 빨리 걸러 비용 낭비 줄이기

특히 웹앱에서 이 로직을 서버 컴포넌트/빌드 과정에 잘못 섞으면 빌드 시간/메모리 문제가 커질 수 있습니다. Next.js 환경에서 빌드가 느려지거나 OOM이 난다면 Next.js 14 빌드 OOM·느려짐 해결 - SWC 캐시·메모리 튜닝 같은 관점으로 “호출 위치”와 “캐시 전략”을 분리하는 게 중요합니다.

Self-Consistency 튜닝 포인트

temperature를 올릴수록 좋은가

아닙니다. 너무 높이면 답의 분산이 커져 합의가 안 생깁니다.

권장 접근:

  • 기본 temperature를 0.7~0.9로 두고
  • 샘플마다 약간씩만 변화
  • 합의가 약하면 n을 늘리기 전에 정규화출력 스키마를 먼저 강화

투표가 깨지는 대표적인 케이스

  • 답이 문장 형태로 다양하게 표현됨
  • 단위/포맷이 제각각
  • JSON이 아니라 설명이 섞여 나옴

해결 순서:

  1. 출력 스키마 강제
  2. 정규화 강화
  3. 그래도 안 되면 답을 “라벨”로 바꾸기(예: A, B, C 중 하나)

보안/정책 관점: CoT 미노출의 장점

  • 내부 추론 노출을 줄여 프롬프트/정책 유출 위험 감소
  • 사용자에게 불필요한 장황한 설명을 줄여 UX 개선
  • “설명”이 틀렸는데 “정답”만 맞는 상황에서 혼란 방지

다만, CoT를 숨긴다고 해서 모델이 안전해지는 것은 아닙니다. 여전히 입력 데이터와 출력 데이터에 대한 필터링, 툴 호출 권한, 로그 마스킹이 필요합니다.

마무리: Self-Consistency는 제품형 LLM의 기본 옵션

Self-Consistency는 “모델을 바꾸지 않고도” 정확도를 올리는 가장 현실적인 수단 중 하나입니다. 핵심은 다음 3가지입니다.

  • CoT를 출력하지 않도록 출력 형식 고정
  • 샘플링 다양성을 주되 early stop으로 비용 제어
  • 정규화·검증을 넣어 투표가 성립하는 시스템으로 만들기

이 3가지만 잡아도, 단발 호출 대비 체감 정확도가 꽤 올라가고, 무엇보다 운영 가능한 형태로 안정화됩니다.