- Published on
Self-Consistency CoT - k샘플·투표로 정답률 올리기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 추론 경로를 여러 번 생성해 놓고, 그중 가장 일관되게 등장하는 답을 선택하면 왜 정확도가 오를까요? Self-Consistency CoT는 이 직관을 체계화한 기법입니다. 단일 CoT는 우연히 잘못된 경로로 빠지면 그대로 오답이 되지만, k번 샘플링하면 오류가 분산되고 다수결이 잡음을 상쇄합니다. 특히 수학·논리·멀티스텝 의사결정처럼 “경로 의존적” 문제에서 효과가 큽니다.
이 글에서는 Self-Consistency CoT의 핵심 아이디어, k-샘플링과 투표 설계, 비용 대비 효율 튜닝, 그리고 RAG/프로덕션 적용 시의 함정을 코드와 함께 정리합니다.
Self-Consistency CoT란 무엇인가
Self-Consistency는 간단히 말해 아래 절차입니다.
- 동일한 질문에 대해 CoT를
k번 생성한다(샘플링을 켠다). - 각 응답에서 “최종 답(final answer)”을 추출한다.
- 최종 답을 기준으로 다수결(또는 가중 투표)로 하나를 고른다.
여기서 중요한 포인트는 “CoT의 문장 자체를 합치는 것”이 아니라 “최종 답을 합의(consensus)로 고른다”는 점입니다. 즉, 경로는 다양하되 결론이 반복되는 답을 채택합니다.
왜 단일 CoT보다 강한가
- 단일 샘플은 편향된 한 경로에 올인합니다.
- 샘플링을 통해 서로 다른 경로를 탐색하면, 일부 경로가 실패해도 다른 경로가 성공할 확률이 생깁니다.
- 다수결은 독립적인 오류를 평균화합니다(오류가 완전히 독립은 아니지만, 온도와 프롬프트 설계로 다양성을 확보하면 효과가 납니다).
언제 효과가 크고, 언제 미묘한가
잘 맞는 케이스
- 수학/계산/논리 퍼즐
- 단계적 계획 수립(제약조건이 많은 일정/조합 최적화의 근사)
- 코드 리뷰나 버그 원인 추론처럼 “가능한 가설”이 여러 개인 문제
효과가 제한적인 케이스
- 사실 기반 단답(예: 특정 날짜/고유명사)처럼 샘플링이 오히려 환각을 늘릴 수 있는 문제
- 답이 길고 서술형이며 정답 판정이 애매한 문제(투표 기준을 만들기 어렵습니다)
이런 경우에는 Self-Consistency보다 검색 근거를 강화하는 RAG, 혹은 리랭킹/검증기를 붙이는 편이 낫습니다. RAG 성능을 끌어올리는 튜닝은 RAG용 Qdrant HNSW 튜닝 실전 가이드도 함께 참고하면 좋습니다.
핵심 설계 1: k, temperature, top_p는 어떻게 잡나
Self-Consistency의 성패는 “다양한 경로를 충분히 뽑되, 무의미한 랜덤성으로 붕괴하지 않게” 균형을 잡는 데 있습니다.
실무에서 자주 쓰는 시작점
k: 5 또는 7부터 시작(비용 대비 효율이 좋습니다)temperature: 0.7 전후(논리 문제는 0.5부터)top_p: 0.9 전후
k를 늘리면 무조건 좋나
아닙니다.
- 초반에는 정확도가 빠르게 오르다가, 어느 순간부터 수익 체감이 옵니다.
- 지연시간과 비용이 선형으로 증가합니다.
실전에서는 목표 SLO(예: p95 2초)와 토큰 비용 한도를 먼저 정하고, 그 안에서 k를 최대한 키우는 방식이 현실적입니다.
핵심 설계 2: “무엇에 투표할 것인가”
투표 단위가 중요합니다. CoT 전체에 투표하면 문장 유사도 문제로 깨지기 쉽고, 최종 답만 투표하면 파싱이 관건입니다.
권장: 구조화된 최종 답 강제
프롬프트에서 최종 답을 JSON으로 내게 하면 투표가 쉬워집니다. 단, MDX 빌드 이슈를 피하려면 본문에서 부등호는 반드시 인라인 코드로 처리해야 합니다.
아래는 “추론은 자유롭게, 최종 답은 JSON” 패턴입니다.
지시사항:
- 문제를 단계적으로 풀되, 최종 답은 반드시 아래 JSON 스키마로만 출력하세요.
- JSON 외 텍스트를 출력하지 마세요.
출력 스키마:
{
"final": "정답(문자열)",
"confidence": 0.0
}
이후 final 필드만 모아 다수결을 하면 됩니다.
숫자/식 답안은 정규화가 필수
예를 들어 1/2, 0.5, 50%는 같은 의미지만 문자열 투표는 서로 다른 값으로 취급합니다. 따라서 투표 전에 정규화(normalization)를 해야 합니다.
- 공백 제거, 소수점 표준화
- 분수
a/b를 유리수로 변환 - 퍼센트는 소수로 변환
구현 예제: Python으로 k-샘플링 후 다수결
아래 예시는 OpenAI 계열 API 호출을 가정한 “패턴 코드”입니다. 실제 SDK/모델명은 환경에 맞게 바꾸면 됩니다.
import json
import re
from collections import Counter
def normalize_answer(ans: str) -> str:
ans = ans.strip()
ans = re.sub(r"\s+", "", ans)
# 간단 예시: 퍼센트 정규화
if ans.endswith("%"):
try:
v = float(ans[:-1]) / 100.0
return str(v)
except ValueError:
pass
return ans
def majority_vote(final_answers):
normed = [normalize_answer(a) for a in final_answers]
c = Counter(normed)
top, cnt = c.most_common(1)[0]
return top, cnt / len(normed)
def extract_final_json(text: str):
# 모델이 JSON만 출력한다는 가정이지만, 방어적으로 파싱
text = text.strip()
return json.loads(text)
def self_consistency_cot(client, prompt: str, k: int = 7, temperature: float = 0.7):
finals = []
raw = []
for _ in range(k):
resp = client.responses.create(
model="gpt-4.1-mini",
input=prompt,
temperature=temperature,
)
out_text = resp.output_text
raw.append(out_text)
obj = extract_final_json(out_text)
finals.append(str(obj.get("final", "")))
voted, vote_ratio = majority_vote(finals)
return {
"voted_final": voted,
"vote_ratio": vote_ratio,
"finals": finals,
"raw": raw,
}
비용 최적화 팁: 병렬 호출
k번을 직렬로 호출하면 지연시간이 커집니다. 서버 환경에서는 비동기/병렬로 쏘는 것이 일반적입니다.
- Python:
asyncio.gather - Node.js:
Promise.all
다만 과도한 병렬화는 레이트리밋에 걸리기 쉽습니다. 결제/크레딧/쿼터 이슈가 있다면 OpenAI Responses API 402 결제·크레딧 오류 해결처럼 운영 관점의 점검도 같이 해두는 편이 안전합니다.
고급: 단순 다수결보다 나은 “가중 투표”
다수결은 강력하지만, 모든 샘플을 동일 가중치로 취급합니다. 실전에서는 아래 신호로 가중치를 줄 수 있습니다.
1) 자체 확신(confidence) 가중
모델이 confidence를 내게 하고, 이를 가중치로 투표합니다. 단, 모델의 자기 확신은 잘못 보정되어 있을 수 있으니(과신) 맹신은 금물입니다.
2) 검증기(verifier) 점수 가중
- 수학이면 정답을 대입해 검산
- 코드면 테스트 실행
- 규칙 기반 제약조건 검사
검증기를 붙이면 Self-Consistency가 “샘플링 앙상블”에서 “샘플링 + 선택”으로 진화합니다.
3) 로그확률 기반(가능한 경우)
일부 API/모델은 토큰 로그확률을 제공하므로, 최종 답의 likelihood를 근거로 가중치를 줄 수 있습니다.
RAG와 결합할 때의 주의점
RAG 파이프라인에서 Self-Consistency를 붙일 때 흔히 하는 실수는 “매 샘플마다 검색 결과가 달라져서 투표가 의미 없어지는” 상황입니다.
패턴 A: 검색은 1회, 생성만 k회(권장)
- 쿼리로 검색을 한 번 수행
- 동일한 컨텍스트를 고정
- 생성만 샘플링해서 k개 답을 뽑고 투표
이렇게 해야 투표가 “추론 다양성”을 반영합니다.
패턴 B: 검색도 k회(주의)
검색도 샘플링하면 컨텍스트가 바뀌면서 답의 분산이 커집니다. 이 경우 투표가 “정답 합의”가 아니라 “서로 다른 근거의 혼합”이 되어 오히려 품질이 흔들릴 수 있습니다.
임베딩/인덱스 변경 등으로 검색 분포 자체가 흔들리는 환경이라면, 임베딩 교체/드리프트 관리 전략도 함께 필요합니다. 관련해서는 Pinecone·Milvus 임베딩 드리프트 탐지와 리인덱싱도 연결해서 보면 전체 파이프라인 안정화에 도움이 됩니다.
프롬프트 패턴: CoT를 노출하지 않으면서도 Self-Consistency 하기
요즘은 정책/보안/제품 요구로 CoT를 그대로 노출하지 않는 경우가 많습니다. 그럴 때는 “내부 추론은 하되, 출력은 짧게”를 강제하고, 여전히 k-샘플링과 투표를 적용할 수 있습니다.
요구사항:
- 내부적으로 단계적 추론을 수행하되, 사용자에게는 추론 과정을 공개하지 마세요.
- 최종 답만 JSON으로 출력하세요.
출력:
{
"final": "...",
"confidence": 0.0
}
이 패턴의 장점은 다음과 같습니다.
- 사용자 출력이 짧아 토큰 비용이 줄어듭니다.
- 투표 파싱이 안정적입니다.
- CoT 노출 리스크를 줄입니다.
운영 관점 체크리스트
Self-Consistency는 “정확도”와 “비용/지연”을 맞바꾸는 기법입니다. 프로덕션에서는 아래를 지표로 관리하는 것이 좋습니다.
1) k별 정확도 곡선
k=1,3,5,7,9에서 오프라인 평가- 수익 체감 구간을 찾아 기본값으로 고정
2) vote ratio 분포
위 코드의 vote_ratio 같은 합의 강도는 품질 신호가 됩니다.
vote_ratio가 높으면(예: 0.7 이상) 답이 안정적- 낮으면(예: 0.4 이하) 불확실, 재질문/추가 검색/검증기 실행 트리거로 사용
3) 실패 모드 로깅
- 파싱 실패(JSON 깨짐)
- 답 정규화 실패
- 레이트리밋/타임아웃
이런 실패는 “모델 성능”이 아니라 “시스템 품질” 문제로 이어지므로 별도 알람을 두는 것이 좋습니다.
자주 하는 실수와 해결책
실수 1: 온도를 너무 낮게 둠
temperature=0.0에 가까우면 k번을 뽑아도 거의 같은 답이 나와 Self-Consistency가 의미가 없어집니다.
- 해결:
temperature를 올리거나top_p를 조정해 경로 다양성을 확보
실수 2: 최종 답 추출이 불안정
자연어로 “정답은 … 입니다”처럼 나오면 파싱이 흔들립니다.
- 해결: JSON 스키마 강제 + 엄격 파서 + 재시도
실수 3: 투표 기준이 애매한 서술형 문제에 적용
서술형 답은 다수결이 “표현”을 뽑을 뿐 “정답”을 뽑지 못합니다.
- 해결: 평가 함수를 정의(루브릭/체커)하거나, 핵심 주장만 구조화해 투표
정리
Self-Consistency CoT는 “한 번의 그럴듯한 추론” 대신 “여러 번의 서로 다른 추론 + 합의”로 정확도를 끌어올리는 실전형 기법입니다. 핵심은 세 가지입니다.
- 샘플 다양성을 만드는 파라미터(
k,temperature,top_p) 튜닝 - 최종 답을 안정적으로 추출·정규화하는 출력 설계(JSON 권장)
- 단순 다수결을 넘어, 검증기/가중 투표로 선택 품질을 강화
RAG와 결합할 때는 “검색은 고정, 생성만 k회”가 기본이며, vote_ratio 같은 합의 강도를 운영 지표로 삼으면 비용을 통제하면서도 품질을 안정화할 수 있습니다.