- Published on
CoT 없이 추론 품질 올리는 SC·ToT 실전
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론에서 먼저 결론부터 말하면, **추론 품질을 올리는 핵심은 “생각을 길게 쓰게 하는 것”이 아니라 “좋은 후보를 더 많이 만들고, 더 잘 고르는 것”**입니다. CoT를 그대로 노출하면 디버깅에는 도움이 되지만, 제품 환경에서는 다음 문제가 자주 발생합니다.
- 보안/프라이버시: 내부 정책, 시스템 프롬프트, 사용자의 민감정보가 추론 과정에 섞여 노출될 수 있음
- 일관성 저하: 길게 말하는 모델이 더 그럴듯해 보이지만, 실제 정답률은 오히려 흔들릴 수 있음
- 비용/지연 증가: 토큰이 늘수록 비용과 p95/p99 지연이 악화
그래서 실무에서는 “CoT를 요구하지 않되” 모델이 다양한 해를 생성하고 검증을 통해 선택하도록 만드는 SC(Self-Consistency)와 ToT(Tree-of-Thoughts)를 많이 씁니다. 이 글은 두 기법을 프롬프트 템플릿, 평가/선택 로직, 운영 팁까지 포함해 실전 관점에서 정리합니다.
참고로, LLM 운영에서 레이트리밋과 재시도 설계는 SC/ToT에 거의 필수로 따라옵니다. 트래픽이 있는 서비스라면 OpenAI 429/RateLimitError 재시도·백오프·큐 설계도 함께 보는 것을 권합니다.
CoT를 “안 쓰는” 게 아니라 “노출하지 않는” 게 목표
많은 팀이 오해하는 지점이 있습니다. CoT를 안 쓴다는 건 모델이 추론을 안 한다는 뜻이 아닙니다. 목표는 보통 다음 중 하나입니다.
- 추론 과정 텍스트를 출력하지 않게(사용자에게 숨김)
- 추론을 강제하는 프롬프트를 피하고, 대신 후보 생성·검증으로 품질을 올림
- 필요하면 내부적으로는 추론을 하되, 출력은 근거 요약/증거 인용/체크리스트 형태로 제한
즉, 출력 포맷을 통제하고, 품질은 샘플링과 선택으로 끌어올리는 접근이 SC/ToT의 실전 가치입니다.
SC(Self-Consistency): “여러 번 풀고 다수결로 고르기”의 정교한 버전
SC는 간단히 말해 동일 문제를 여러 번 독립적으로 풀게 한 뒤, 최종 답을 합의(majority vote) 또는 스코어링으로 선택하는 방식입니다.
SC가 잘 먹히는 문제 유형
- 수학/논리처럼 정답이 명확한 문제
- 규칙 기반 분류(라벨이 제한된 경우)
- 구조화된 출력(예: JSON)에서 필드 값이 안정적이어야 하는 경우
반대로, 창작/요약처럼 “정답이 하나가 아닌” 문제는 단순 다수결이 품질을 보장하지 않습니다. 이때는 ToT나 별도 평가자(critic) 패턴이 더 낫습니다.
SC의 핵심 파라미터
n: 샘플 개수(보통 5~20)temperature: 다양성 확보(보통 0.7~1.2)top_p: 분포 꼬리 사용 여부- 집계 방식: exact match, 정규화 후 match, 임베딩 유사도 클러스터링, 또는 LLM-as-judge
프롬프트 템플릿(추론 노출 없이)
아래는 “과정은 내부적으로 하되, 출력은 짧게”를 강제하는 템플릿입니다. 본문에 부등호가 나오지 않도록 모든 기호는 인라인 코드로 처리합니다.
역할: 당신은 정확성을 최우선으로 하는 해결자입니다.
규칙:
- 풀이 과정(추론)을 출력하지 마세요.
- 최종 답만 간결히 출력하세요.
- 불확실하면 "모름"이라고 답하세요.
문제:
{question}
출력 형식:
final: {answer}
집계 로직 예시(Node.js/TypeScript)
아래 예시는 n번 호출해 답을 모으고, 정규화 후 다수결로 선택합니다. 실무에서는 여기에 재시도/백오프, 동시성 제한, 캐시가 붙습니다.
type Sample = { raw: string; normalized: string };
function normalizeAnswer(s: string) {
return s
.trim()
.toLowerCase()
.replace(/\s+/g, " ")
.replace(/[\.,!]/g, "");
}
function majorityVote(samples: Sample[]) {
const counts = new Map<string, number>();
for (const x of samples) {
counts.set(x.normalized, (counts.get(x.normalized) ?? 0) + 1);
}
let best = "";
let bestCount = -1;
for (const [k, v] of counts) {
if (v > bestCount) {
best = k;
bestCount = v;
}
}
return { answer: best, votes: bestCount, total: samples.length };
}
async function selfConsistencySolve(question: string, n: number) {
const samples: Sample[] = [];
for (let i = 0; i < n; i++) {
const raw = await callLLM({
prompt: buildPrompt(question),
temperature: 0.9,
});
samples.push({ raw, normalized: normalizeAnswer(raw) });
}
return majorityVote(samples);
}
SC에서 자주 터지는 함정 5가지
- 정규화 실패:
"1,000"과"1000"이 다른 답으로 취급됨 - 모호한 답: 답이 문장형이면 다수결이 깨짐(그래서 출력 포맷을 엄격히)
- 샘플 다양성 부족:
temperature가 너무 낮으면n을 늘려도 의미 없음 - 비용 폭발:
n배 비용이므로, 쉬운 문제는n을 줄이는 게이트가 필요 - 집계가 정답을 보장하지 않음: 모델이 같은 틀린 답을 반복하면 다수결도 틀림
이 마지막 문제 때문에, SC는 보통 검증 단계(rule checker, unit test, retrieval evidence, judge model)와 같이 씁니다.
ToT(Tree-of-Thoughts): “후보를 트리로 확장하고 가지치기”
ToT는 한 번에 정답을 뱉게 하기보다, **중간 상태(state)**를 기준으로 여러 선택지를 확장하고, 각 단계에서 **평가(score)**로 유망한 가지를 남기는 탐색 방식입니다.
ToT는 특히 다음 상황에서 강합니다.
- 계획 수립(여행 일정, 프로젝트 플랜, 마이그레이션 단계)
- 복잡한 디버깅/트러블슈팅(가설을 세우고 검증해야 하는 문제)
- 제약 조건이 많은 최적화(우선순위, 비용, 리스크)
예를 들어 장애 대응 글을 쓸 때도 ToT 사고방식이 유효합니다. 원인 후보를 넓게 펼친 뒤 빠르게 가지치기하는 방식은 K8s CrashLoopBackOff 원인 10가지·즉시 진단법 같은 운영 진단과도 결이 같습니다.
ToT의 구성 요소
- State: 현재까지의 부분 해(예: “원인 가설 목록”, “조사 결과”, “다음 실험”)
- Expand: 다음 후보 생성(브레인스토밍)
- Evaluate: 후보를 점수화(규칙, LLM judge, 테스트 통과 여부)
- Select/Prune: 상위
k개만 남김(beam search) - Stop: 종료 조건(목표 달성, 예산 초과, 더 이상 개선 없음)
ToT 프롬프트 패턴(상태와 행동만 출력)
CoT를 길게 노출하지 않기 위해, 중간 단계는 “생각”이 아니라 행동 계획과 관측치로 표현하게 만들면 좋습니다.
당신은 문제 해결 에이전트입니다.
목표:
{goal}
현재 상태(state):
{state_json}
해야 할 일:
1) 다음 후보 행동(action) 3개를 제안
2) 각 행동의 기대효과, 리스크, 비용(낮음/중간/높음)을 표로 출력
3) 가장 좋은 행동 1개를 선택하고, 그 이유를 한 문장으로만 출력
제약:
- 추론 과정 서술 금지
- 출력은 지정한 JSON만
출력(JSON):
{"actions":[...],"selected":"...","why":"..."}
ToT 실행 루프 예시(Python)
아래는 간단한 beam search 형태입니다. 실제 서비스에서는 상태를 DB에 저장하고, 각 노드 평가를 비동기로 돌리며, 예산(토큰/시간)을 넘기면 중단합니다.
from dataclasses import dataclass
from typing import List
@dataclass
class Node:
state: dict
score: float
parent_id: str | None = None
def expand(node: Node) -> List[dict]:
# LLM 호출로 다음 action 후보 3~5개 생성
return call_llm_expand(node.state)
def evaluate(state: dict) -> float:
# 규칙 기반 + judge 모델 점수 결합 가능
return call_llm_judge(state)
def step(frontier: List[Node], beam_k: int) -> List[Node]:
candidates: List[Node] = []
for node in frontier:
for next_state in expand(node):
s = evaluate(next_state)
candidates.append(Node(state=next_state, score=s))
candidates.sort(key=lambda n: n.score, reverse=True)
return candidates[:beam_k]
def tree_of_thoughts(initial_state: dict, steps: int = 3, beam_k: int = 3):
frontier = [Node(state=initial_state, score=0.0)]
for _ in range(steps):
frontier = step(frontier, beam_k)
return frontier[0].state
ToT 평가(Evaluate)를 잘 만드는 법
ToT 성능의 대부분은 평가 함수가 좌우합니다. 실무에서 많이 쓰는 조합은 다음입니다.
- 하드 룰 체크: JSON 스키마 준수, 금칙어, 길이 제한, 필수 필드
- 외부 검증: 코드 실행, SQL 실행, API 응답 확인
- LLM judge: “목표 달성도/리스크/정확성”을 기준으로 0~10 점수
- 근거 기반 점수: RAG를 붙였다면 인용된 근거가 실제로 답을 지지하는지
도구 호출(JSON 스키마)에서 자주 겪는 문제는 스키마 미스매치입니다. 툴-콜 기반 ToT를 운영한다면 Claude Tool Use 400 오류 - schema·JSON 해결 가이드처럼 스키마를 엄격히 고정하는 게 중요합니다.
SC vs ToT: 언제 무엇을 쓰나
| 기준 | SC | ToT |
|---|---|---|
| 목표 | 정답률 안정화 | 복잡 문제의 탐색/계획 |
| 비용 | 호출 n배(선형) | 단계 steps와 beam_k에 따라 급증 가능 |
| 구현 난이도 | 낮음 | 중간~높음(상태/평가/가지치기 필요) |
| 잘 맞는 문제 | 단답형, 라벨링, 계산 | 계획, 디버깅, 제약 최적화 |
| 핵심 포인트 | 다양성 확보 + 좋은 집계 | 평가 함수 품질 + 탐색 전략 |
실무에서는 다음 하이브리드가 흔합니다.
- 1단계(ToT): 후보 해결 전략을 3가지로 확장
- 2단계(SC): 각 전략을 5번씩 실행해 안정화
- 3단계(검증): 규칙/테스트/근거로 최종 필터
운영 관점: 비용·지연·레이트리밋을 어떻게 감당하나
SC/ToT는 본질적으로 호출 수를 늘립니다. 그래서 운영 설계가 없으면 금방 터집니다.
1) 난이도 게이팅
- 쉬운 요청은
n=1로 끝내고, 애매할 때만 SC/ToT를 켭니다. - 애매함 신호 예시: 모델이
"모름"을 반환, 낮은 자신감 점수, 규칙 검증 실패
2) 예산 기반 중단
max_tokens_budget또는max_time_ms를 정하고 초과 시 best-so-far 반환- ToT는
steps를 고정하기보다 “개선이 없으면 중단” 조건이 효율적
3) 동시성 제한 + 백오프
- SC는 병렬 호출 유혹이 크지만, 계정 레이트리밋에 바로 걸립니다.
- 큐/세마포어로 동시성 상한을 두고, 429에는 지수 백오프를 적용합니다.
4) 캐시
- 동일 질문, 동일 컨텍스트면 SC 결과를 캐시해
n배 비용을 줄입니다. - ToT는 상태가 길어질수록 캐시 키 설계가 어려우니 “초기 상태 해시 + 목표” 정도로 단순화하는 편이 낫습니다.
실전 체크리스트(바로 적용용)
- 출력에 CoT를 요구하지 말고, 포맷 제약(단답, JSON)을 먼저 고정했는가
- SC는
temperature를 올려 샘플 다양성을 확보했는가 - 다수결 전에 정규화/파싱을 제대로 했는가
- ToT는 상태를 “서술”이 아니라 관측치/결정/다음 행동 중심으로 설계했는가
- Evaluate가 주관적 문장 평가에 머물지 않고 룰/테스트/근거를 포함하는가
- 호출 수 증가에 대비해 게이팅, 예산 중단, 동시성 제한, 백오프, 캐시가 있는가
마무리
CoT를 사용자에게 그대로 노출하지 않아도, SC와 ToT로 후보 생성과 선택의 품질을 올리면 추론 성능은 충분히 개선됩니다. SC는 “여러 번 풀고 합의로 고르기”로 간단하게 안정성을 확보하고, ToT는 “탐색과 가지치기”로 복잡한 문제에서 구조적으로 정답에 가까워집니다.
다음 단계로는, 여러분의 도메인에서 검증 가능한 평가 함수(룰/테스트/근거)를 먼저 만들고, 그 위에 SC와 ToT를 얹어보는 것을 추천합니다. 이렇게 하면 CoT 없이도 “설명은 짧지만 결과는 강한” 시스템을 만들 수 있습니다.