- Published on
Self-Consistency로 CoT 신뢰도 높이는 실전
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
LLM을 제품에 붙여보면 금방 마주치는 문제가 있습니다. 같은 프롬프트인데도 한 번은 정답, 한 번은 그럴듯한 오답을 내는 현상입니다. 특히 Chain-of-Thought(이하 CoT) 기반 추론은 “설명”이 붙기 때문에 더 믿음직해 보이지만, 설명이 논리적으로 그럴싸해도 결론이 틀릴 수 있습니다.
이때 현업에서 가장 빨리 효과를 보는 방법 중 하나가 Self-Consistency입니다. 아이디어는 단순합니다. 같은 문제를 여러 번 풀게 한 뒤, 가장 일관되게 등장하는 답을 채택합니다. CoT를 “잘 쓰는 법”보다 “잘 검증하는 법”에 가깝고, 구현 난이도 대비 체감 성능이 좋습니다.
아래에서는 Self-Consistency의 원리, 언제 효과적인지, 서비스 적용 시 비용과 지연을 어떻게 제어하는지, 그리고 바로 가져다 쓸 수 있는 코드 패턴까지 실전 관점으로 정리합니다.
Self-Consistency란 무엇인가
Self-Consistency는 2022년경부터 널리 알려진 추론 강화 기법으로, 요약하면 다음과 같습니다.
- 동일한 질문에 대해 서로 다른 추론 경로를 여러 개 샘플링한다
- 각 샘플이 제시한 최종 답을 수집한다
- 최종 답들에 대해 다수결(majority vote) 혹은 가중 투표를 수행한다
- 선택된 답을 최종 출력으로 반환한다
핵심은 “추론 과정의 다양성”입니다. 따라서 보통 temperature를 0으로 고정하는 대신, 적당히 올려서 다양한 경로를 생성합니다.
CoT와의 관계
CoT가 “한 번의 길게 생각하기”라면, Self-Consistency는 “여러 번 다르게 생각해 보고 가장 많이 나온 결론을 고르기”입니다.
중요한 점은, Self-Consistency는 CoT를 반드시 요구하지 않습니다. 하지만 실제로는 CoT를 함께 쓰는 편이 유리합니다.
- CoT는 중간 추론을 노출(또는 내부적으로 생성)하여 다양한 경로 샘플링을 쉽게 만든다
- Self-Consistency는 그 다양한 경로 중 우연히 맞은 한 번이 아니라 반복적으로 맞는 패턴을 선택한다
언제 Self-Consistency가 특히 잘 먹히나
Self-Consistency는 모든 문제에서 만능은 아닙니다. “정답이 수렴하는 문제”에서 강합니다.
효과가 좋은 유형
- 수학/논리 퍼즐/정형 추론: 답 공간이 작고 정답이 명확
- 규칙 기반 변환: 예를 들어 포맷팅 규칙, 간단한 파싱 규칙
- 도메인 지식이 충분히 들어간 폐쇄형 질의: 문서 기반 QA에서 근거가 명확한 경우
효과가 제한적인 유형
- 창의적 글쓰기: 정답이 없고 다양성이 목적
- 의견/취향: 다수결이 “정답”을 의미하지 않음
- 답 공간이 매우 큰 생성형 작업: 예를 들어 코드 생성에서 완전 동일한 정답 문자열을 다수결로 뽑기 어려움
이런 경우엔 Self-Consistency를 “최종 문자열”이 아니라 “검증 가능한 구조”로 투표하도록 바꾸는 것이 중요합니다. 예를 들어 JSON의 특정 필드, 혹은 계산 결과 숫자만 투표 대상으로 삼는 식입니다.
실전 설계: 무엇을 투표할 것인가
현업 적용에서 가장 흔한 실패는 “그냥 출력 전체를 투표”하려는 시도입니다. 샘플마다 표현이 달라서 표가 갈리고, 다수결이 깨집니다.
따라서 투표 대상은 가능한 한 정규화된 값이어야 합니다.
추천 패턴 1: 최종 답만 강제 추출
프롬프트에서 최종 답을 특정 태그나 키로만 출력하게 하고, 그 값만 투표합니다.
- 출력 포맷 예:
final_answer필드 - CoT는 숨기거나(내부 추론), 혹은 별도 필드로 분리
추천 패턴 2: 구조화된 중간 산출물 투표
예를 들어 “정답과 근거 문장 id”를 같이 내게 하고, 정답은 다수결로, 근거는 정답을 낸 샘플들 중 점수가 높은 것을 선택합니다.
이 패턴은 RAG에서 특히 유용합니다. 검색이 흔들리면 답도 흔들립니다. 검색·리랭킹을 먼저 안정화하는 것도 중요하니, 필요하면 RAG 회수율 급락? 하이브리드+리랭커 튜닝도 함께 참고하면 좋습니다.
파라미터 튜닝: n, temperature, top_p의 현실적인 기준
Self-Consistency의 품질은 샘플 수 n에 크게 좌우되지만, 비용과 지연도 선형으로 증가합니다.
권장 출발점
n: 5 또는 7temperature: 0.7 전후top_p: 0.9 전후
정답이 매우 불안정하면 n을 늘리기 전에 먼저 아래를 점검합니다.
- 프롬프트가 과도하게 장황해서 모델이 핵심 제약을 놓치지 않는가
- 출력 포맷이 애매해서 파싱/정규화가 깨지지 않는가
- RAG라면 컨텍스트가 과다/부족하지 않은가
비용/지연을 줄이는 2단계 전략
항상 n=7로 돌리면 비용이 큽니다. 실전에서는 적응형(Adaptive) Self-Consistency가 잘 맞습니다.
- 먼저
n=3으로 샘플링 - 다수결이 강하게 나왔으면(예: 3개 중 3개 동일) 즉시 종료
- 표가 갈리면
n=7까지 추가 샘플링
이 방식은 평균 비용을 크게 낮춥니다.
구현 예시: Node.js로 Self-Consistency 투표기 만들기
아래 예시는 OpenAI 호환 Chat Completions 스타일을 가정한 “개념 코드”입니다. 핵심은
- 출력에서
final_answer만 뽑는다 - 정규화해서 키로 만든다
- 다수결로 선택한다
import crypto from "node:crypto";
function normalizeAnswer(s) {
return String(s)
.trim()
.replaceAll(/\s+/g, " ")
.toLowerCase();
}
function majorityVote(items) {
const count = new Map();
for (const x of items) {
count.set(x, (count.get(x) ?? 0) + 1);
}
let best = null;
let bestN = -1;
for (const [k, v] of count.entries()) {
if (v > bestN) {
best = k;
bestN = v;
}
}
return { winner: best, votes: bestN, dist: count };
}
async function callLLM({ client, prompt, temperature }) {
// client는 OpenAI SDK 또는 호환 클라이언트라고 가정
const res = await client.chat.completions.create({
model: "gpt-4.1-mini",
temperature,
top_p: 0.9,
messages: [
{
role: "system",
content:
"You are a careful reasoner. Output JSON only with keys: final_answer, confidence." +
" Do not include extra keys.",
},
{ role: "user", content: prompt },
],
response_format: { type: "json_object" },
});
const text = res.choices[0].message.content;
const obj = JSON.parse(text);
return {
final_answer: obj.final_answer,
confidence: obj.confidence,
raw: obj,
};
}
export async function selfConsistency({ client, prompt, n = 5 }) {
const temperature = 0.7;
const samples = [];
for (let i = 0; i < n; i++) {
const s = await callLLM({ client, prompt, temperature });
samples.push(s);
}
const normalized = samples.map((s) => normalizeAnswer(s.final_answer));
const { winner, votes, dist } = majorityVote(normalized);
// winner에 해당하는 원본 샘플 하나를 대표로 선택(가장 높은 confidence)
const candidates = samples
.map((s, idx) => ({ ...s, idx, norm: normalized[idx] }))
.filter((s) => s.norm === winner)
.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
const picked = candidates[0];
return {
answer: picked.final_answer,
votes,
n,
dist: Object.fromEntries([...dist.entries()]),
sample_hash: crypto
.createHash("sha256")
.update(JSON.stringify(samples))
.digest("hex"),
};
}
포인트
response_format을 JSON으로 강제해 파싱 안정성을 높였습니다.- 다수결은 정규화된 문자열 기준으로 수행합니다.
- 동률 처리, 숫자/단위 정규화(예:
1,000vs1000) 같은 디테일을 추가하면 안정성이 더 올라갑니다.
고급 패턴: 다수결만으로 부족할 때
Self-Consistency는 “가장 자주 나온 답”을 고르지만, 다수가 틀릴 수도 있습니다. 특히 모델이 특정 편향된 휴리스틱에 빠지면 오답이 다수파가 되기도 합니다.
1) 가중 투표: 모델의 자기평가를 그대로 믿지 말고 보정하기
샘플마다 confidence를 받더라도, 모델의 자기 확신은 종종 과대평가됩니다. 그래도 완전히 버리기보다는 다음처럼 제한적으로 씁니다.
- 기본은 다수결
- 다수결이 약할 때(예: 7개 중 3표, 2표, 2표)만 confidence 합으로 타이브레이크
2) 검증자(Verifier) 추가
“답을 고르는 모델”과 “답을 검증하는 모델”을 분리하면 성능이 더 오릅니다.
- 1단계: Self-Consistency로 후보 답
k개 선정 - 2단계: Verifier가 각 후보를 채점하고 1개 선택
이 구조는 지연이 늘지만, 고위험 도메인(금융/의료/보안)에서 유용합니다.
3) 도메인 규칙 기반 체크
정답이 숫자라면 간단한 산술 검증, 포맷이 정해져 있으면 스키마 검증을 붙이세요. “LLM을 LLM로만 검증”하면 비용이 커지고 실패 모드가 겹칩니다.
운영 관점: 재현성, 관측성, 장애 대응
Self-Consistency는 본질적으로 샘플링을 쓰므로, 운영에서 다음이 중요합니다.
재현성
- 요청별로
seed를 고정할 수 있는 API라면 seed를 저장 - 불가능하다면 최소한 입력 프롬프트, 모델 버전, 파라미터, 샘플 결과 해시를 저장
위 코드의 sample_hash는 “이 요청에서 어떤 샘플들이 나왔는지”를 추적하기 위한 최소 장치입니다.
관측성 지표
vote_strength: 최다 득표 수 나누기n(예:5/7)entropy: 분포가 얼마나 퍼져 있는지disagreement_rate: 1표라도 다른 답이 나온 비율
이 지표들은 “언제 n을 늘려야 하는지”, “언제 verifier로 넘겨야 하는지”를 결정하는 신호가 됩니다.
지연 최적화
샘플을 순차로 돌리면 느립니다. 가능하면 병렬 호출을 쓰세요.
export async function selfConsistencyParallel({ client, prompt, n = 7 }) {
const temperature = 0.7;
const tasks = Array.from({ length: n }, () =>
callLLM({ client, prompt, temperature })
);
const samples = await Promise.all(tasks);
const normalized = samples.map((s) => normalizeAnswer(s.final_answer));
const { winner, votes, dist } = majorityVote(normalized);
return {
answer: samples.find((s, i) => normalized[i] === winner)?.final_answer,
votes,
n,
dist: Object.fromEntries([...dist.entries()]),
};
}
병렬 호출은 레이트 리밋과 비용 폭증 리스크가 있으니, 큐잉/동시성 제한을 함께 두는 편이 안전합니다.
LLM 서빙을 KServe나 vLLM로 운영 중이라면, 다중 샘플링은 502나 타임아웃을 더 자주 유발할 수 있습니다. 인퍼런스 장애를 잡는 루틴은 KServe vLLM 배포 후 502? InferenceService 8단계를 참고해 “인프라 레벨 병목”부터 제거하세요.
RAG + Self-Consistency: 검색 흔들림까지 포함해 안정화하기
RAG에서는 “모델 추론의 불확실성” 외에 “검색 결과의 불확실성”이 추가됩니다. 이때 Self-Consistency를 적용하는 방법은 두 가지입니다.
- 고정 컨텍스트: 검색은 1회만 하고, 같은 컨텍스트로
n번 추론 - 가변 컨텍스트: 검색도 매번(또는 일부) 다시 하고, end-to-end로
n번 샘플링
일반적으로는 1번이 비용 대비 효율이 좋습니다. 2번은 검색 자체가 불안정하거나, 리랭커가 확률적 요소를 포함할 때 고려합니다.
또한 “근거가 있는 답만 채택”하도록 정책을 두면 환각을 줄일 수 있습니다.
- 답과 함께
citations(문서 id, 문장 id)를 강제 - citations가 비어 있으면 해당 샘플은 투표에서 제외
보안/정확도 요구가 높은 업무에서의 체크리스트
Self-Consistency를 붙였다고 해서 “정확도 문제가 해결”되지는 않습니다. 대신 실패 모드를 더 빨리 탐지하고 불확실한 케이스를 격리할 수 있어야 합니다.
vote_strength가 낮으면 “확신 낮음”으로 응답하거나 사람 검토로 라우팅- 중요한 작업은 Verifier 또는 규칙 검증을 추가
- 프롬프트 인젝션 방어는 별개로 필요(특히 RAG)
분산 트랜잭션이나 보상 트랜잭션 같은 영역에서는 “한 번의 잘못된 판단”이 중복 실행으로 이어질 수 있습니다. LLM을 오케스트레이션에 쓰는 경우, 멱등성/중복 방지 설계는 반드시 별도로 챙겨야 합니다. 관련해서는 Saga 패턴에서 보상 트랜잭션 중복 실행 막는 법도 함께 보면 연결 지점이 많습니다.
실전 적용 순서: 가장 안전한 롤아웃
- 출력 포맷 고정: JSON 스키마로
final_answer분리 - 정규화/투표 로직 구현: 문자열, 숫자, 단위 정규화 포함
- 적응형
n: 먼저n=3, 불일치 시 확장 - 관측성 추가: 분포/합의 강도 지표 로깅
- 고위험 구간에만 Verifier: 전체가 아니라 예외 케이스에만 적용
이 순서대로 가면 “성능 향상”과 “비용/지연 통제”를 동시에 달성하기 쉽습니다.
마무리
Self-Consistency는 CoT의 장점을 살리면서도, 단발성 샘플링의 불안정성을 완화하는 실전형 기법입니다. 다만 성공의 관건은 n을 무작정 늘리는 것이 아니라, 무엇을 투표할지(정규화된 최종 답), 언제 더 샘플링할지(적응형 종료), **불확실성을 어떻게 운영 정책으로 흡수할지(관측성과 라우팅)**에 있습니다.
CoT를 “잘 말하게” 만드는 것만큼, “일관되게 맞게” 만드는 장치를 함께 두면 LLM 기능을 제품에 넣었을 때의 신뢰도가 확실히 달라집니다.