Published on

CoT 없이 추론 유도 - SC·ToT 실전 가이드

Authors

서버에 LLM을 붙여보면 금방 부딪히는 문제가 있습니다. 답이 그럴듯해 보이는데도 재현이 안 되거나, 같은 질문에 매번 다른 결론이 나오거나, 디버깅 가능한 근거를 남기기 어렵다는 점입니다. 많은 팀이 이를 해결하려고 Chain-of-Thought(이하 CoT)를 길게 출력하게 만들지만, 운영 관점에서는 두 가지 리스크가 큽니다.

  • 민감 정보 노출: 내부 정책, 고객 데이터, 시스템 프롬프트 일부가 “추론 과정”에 섞여 나올 수 있습니다.
  • 품질 관리 어려움: 장황한 추론이 정답을 보장하지 않고, 오히려 그럴듯한 오류를 강화할 수 있습니다.

그래서 최근 실무에서는 추론을 “하게” 만들되, 그 과정을 “노출하지 않게” 설계하는 패턴이 중요해졌습니다. 이 글은 그중 대표적인 두 축인 SC(Self-Consistency)ToT(Tree-of-Thought)CoT 비노출 방식으로 적용하는 방법을 실전 관점에서 정리합니다.

왜 CoT를 숨기면서도 추론을 유도해야 하나

CoT는 모델의 중간 생각을 텍스트로 풀어내는 방식이라, 다음과 같은 운영 요구와 충돌합니다.

  1. 감사/보안: 로그에 남는 텍스트는 곧 데이터입니다. 추론 로그가 길수록 위험 표면이 커집니다.
  2. 비용/지연: 토큰이 늘면 비용과 레이턴시가 같이 증가합니다.
  3. 평가 난이도: 중간 과정이 길어질수록 “정답 여부”보다 “설명 설득력”이 커져서 평가가 왜곡됩니다.

대신 우리가 원하는 것은 아래입니다.

  • 모델이 내부적으로는 다양한 가설을 탐색하고
  • 최종 출력은 짧고 검증 가능하며
  • 필요하면 근거는 구조화된 요약 정도로만 남기는 것

이를 위한 대표적 도구가 SC와 ToT입니다.

SC(Self-Consistency): 여러 번 뽑아 다수결로 정답을 고정

SC는 간단히 말해 같은 문제를 여러 번 풀게 하고, 결과를 집계해 가장 일관된 답을 선택하는 전략입니다. CoT를 노출하지 않아도 효과가 큽니다.

언제 SC가 특히 잘 먹히나

  • 정답 공간이 이산적일 때: 분류, 선택지, 라벨링, yes/no, 원인 후보 리스트 등
  • 단일 추론 경로가 불안정할 때: 온도(temperature)를 조금만 올려도 답이 흔들리는 문제
  • 정답 검증 규칙이 있을 때: 포맷 검증, 스키마 검증, 간단한 룰 기반 체크

반대로, 창작형/서술형처럼 “정답이 하나”가 아닌 문제는 SC가 비용 대비 효율이 떨어질 수 있습니다.

CoT 없이 SC를 거는 프롬프트 패턴

핵심은 중간 추론을 쓰지 말고, 대신 결론만 구조화해서 내게 하는 것입니다.

시스템: 당신은 신뢰성 높은 분석가입니다.
규칙:
- 중간 추론 과정은 출력하지 마세요.
- 최종 답만 JSON으로 출력하세요.

사용자: 아래 장애 요약을 읽고 가장 가능성이 높은 원인 카테고리 1개를 고르세요.
카테고리: [DNS, 네트워크 정책, 인증, 레이트리밋, 앱 버그]
출력 형식: {"label": "...", "confidence": 0~1}
장애 요약: ...

이렇게 하면 모델은 내부적으로는 추론을 하되, 출력은 짧고 집계하기 쉬워집니다.

SC 집계 구현 예제 (Node.js)

아래는 동일 입력을 n번 호출하고, label 다수결과 평균 confidence로 최종 결론을 고르는 예시입니다. API는 어떤 벤더든 동일한 형태로 적용 가능합니다.

import crypto from "crypto";

async function callModel({ prompt, temperature }) {
  // 실제 구현에서는 OpenAI/사내 게이트웨이 등을 호출
  // 여기서는 { label, confidence } JSON을 반환한다고 가정
  return fetch("https://api.example.com/llm", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ prompt, temperature })
  }).then(r => r.json());
}

function majorityVote(results) {
  const counts = new Map();
  const confSum = new Map();

  for (const r of results) {
    counts.set(r.label, (counts.get(r.label) ?? 0) + 1);
    confSum.set(r.label, (confSum.get(r.label) ?? 0) + (r.confidence ?? 0));
  }

  let best = null;
  for (const [label, c] of counts.entries()) {
    const avgConf = (confSum.get(label) ?? 0) / c;
    const candidate = { label, votes: c, avgConfidence: avgConf };
    if (!best) best = candidate;
    else if (candidate.votes > best.votes) best = candidate;
    else if (candidate.votes === best.votes && candidate.avgConfidence > best.avgConfidence) best = candidate;
  }
  return best;
}

export async function selfConsistentLabel(prompt, n = 7) {
  const runId = crypto.randomUUID();
  const temps = Array.from({ length: n }, () => 0.7);

  const results = [];
  for (let i = 0; i < n; i++) {
    const out = await callModel({ prompt, temperature: temps[i] });
    results.push(out);
  }

  const decision = majorityVote(results);
  return { runId, decision, samples: results };
}

운영 팁: SC는 “재시도”가 아니라 “앙상블”이다

SC는 실패 시 재시도가 아니라, 의도적으로 다양성을 확보하기 위한 반복입니다. 따라서 다음을 분리해야 합니다.

  • 전송/서버 오류 재시도: 500, 503, 타임아웃 등
  • 추론 다양성 반복(SC): 정상 응답을 여러 번 받아 집계

이 두 층을 섞으면 비용이 폭발하거나, 장애 시나리오에서 결과가 왜곡됩니다. 재시도/폴백/서킷브레이커는 별도 계층으로 두는 게 안전합니다. 관련 패턴은 이 글을 같이 참고하면 좋습니다: OpenAI Responses API 500·503 대응 재시도 폴백 서킷브레이커

ToT(Tree-of-Thought): 후보 계획을 “탐색”하고 최적 경로를 고르는 방법

ToT는 한 번에 결론을 내지 않고, 여러 후보(가지)를 만들고 평가하며 탐색하는 방식입니다. CoT를 출력하지 않아도, “가지 생성”과 “가지 평가”를 API 호출 단위로 분리하면 충분히 구현 가능합니다.

ToT가 필요한 문제의 특징

  • 문제 풀이가 여러 단계 의사결정으로 구성됨
  • 중간 단계에서 잘못된 선택을 하면 이후가 전부 무너짐
  • 단순 다수결로는 해결이 안 되고, 탐색과 평가 함수가 필요함

예: 장애 대응 런북 생성, 마이그레이션 플랜 수립, 테스트 전략 설계, 성능 튜닝 순서 결정 등

CoT 비노출 ToT의 핵심 설계

ToT를 “생각을 길게 쓰는 프롬프트”로 오해하면 안 됩니다. 실무형 ToT는 보통 다음처럼 나눕니다.

  1. Generate(가지 생성): 후보 계획을 여러 개 생성하되, 각 후보는 짧은 bullet과 체크리스트로
  2. Score(가지 평가): 별도 호출로 후보를 점수화(정확성/리스크/비용/시간)
  3. Select(선택): 최고 점수 후보를 선택
  4. Refine(정제): 선택된 후보만 구체화

중간 추론은 숨기고, 각 단계 출력은 구조화된 결과물로 제한합니다.

ToT 프롬프트 템플릿

1) 가지 생성

시스템: 당신은 SRE 플래너입니다.
규칙:
- 중간 추론은 출력하지 마세요.
- 후보는 최대 4개.
- 각 후보는 "전략 요약" 1줄 + "실행 단계" 5개 이내.

사용자: 상황: EKS 환경에서 특정 워크로드가 IPv6 only에서 외부 통신이 실패한다.
가능한 점검 플랜 후보를 4개 만들어라.
출력은 JSON 배열로.

이런 케이스는 실제로 네트워크/CNI/보안그룹/DNS 등 교차 원인이 많아 ToT가 유리합니다. 관련 트러블슈팅 관점은 EKS에서 IPv6만 통신 실패 - CNI·SG·DNS 점검도 함께 보면 ToT 평가 기준을 만들기 쉽습니다.

2) 가지 평가

시스템: 당신은 계획 평가자입니다.
규칙:
- 중간 추론은 출력하지 마세요.
- 각 후보를 다음 기준으로 0~10점 평가: 현실성, 리스크, 소요시간, 커버리지
- 출력은 후보별 점수와 총점, 코멘트 1줄

사용자: 아래 후보 계획들을 평가해라.
후보들: ...

3) 선택 및 정제

시스템: 당신은 실행 가능한 런북 작성자입니다.
규칙:
- 중간 추론은 출력하지 마세요.
- 선택된 계획을 실제 런북 형태로 구체화: 명령어 예시, 관측 지표, 실패 시 분기

사용자: 최고 점수 후보를 기반으로 런북을 작성해라.

ToT 구현 예제 (Python)

아래 예시는 generate로 후보 4개를 만들고, score로 점수화한 뒤 최종 런북을 생성합니다.

import json
from typing import List, Dict, Any


def llm_call(payload: Dict[str, Any]) -> Dict[str, Any]:
    # 실제로는 사내 게이트웨이 또는 벤더 SDK 호출
    # 여기서는 {"text": "..."} 형태를 가정
    raise NotImplementedError


def generate_candidates(context: str) -> List[Dict[str, Any]]:
    prompt = {
        "role": "system",
        "content": "후보는 최대 4개. 중간 추론은 출력하지 말고 JSON 배열만 출력."
    }
    user = {
        "role": "user",
        "content": f"상황: {context}\n점검 플랜 후보 4개를 JSON 배열로 생성해라."
    }
    out = llm_call({"messages": [prompt, user], "temperature": 0.7})
    return json.loads(out["text"])


def score_candidates(context: str, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    prompt = {
        "role": "system",
        "content": "각 후보를 현실성/리스크/소요시간/커버리지 0~10점으로 평가. JSON만 출력."
    }
    user = {
        "role": "user",
        "content": f"상황: {context}\n후보: {json.dumps(candidates, ensure_ascii=False)}"
    }
    out = llm_call({"messages": [prompt, user], "temperature": 0.2})
    return json.loads(out["text"])


def pick_best(scored: List[Dict[str, Any]]) -> Dict[str, Any]:
    return max(scored, key=lambda x: x.get("total", 0))


def refine_runbook(context: str, best: Dict[str, Any]) -> str:
    prompt = {
        "role": "system",
        "content": "중간 추론 없이 런북을 작성. 체크리스트/명령어/관측지표/분기 포함."
    }
    user = {
        "role": "user",
        "content": f"상황: {context}\n선택된 후보: {json.dumps(best, ensure_ascii=False)}\n런북을 마크다운으로 작성해라."
    }
    out = llm_call({"messages": [prompt, user], "temperature": 0.3})
    return out["text"]

이 구조의 장점은 명확합니다.

  • 모델이 내부적으로는 탐색을 하되, 로그에는 후보/점수/선택만 남습니다.
  • 평가 단계에서 일관된 기준을 강제할 수 있습니다.
  • 특정 단계만 교체 가능: 예를 들어 score는 규칙 기반 평가기로 대체할 수 있습니다.

SC와 ToT를 같이 쓰는 하이브리드 패턴

현장에서 가장 강력한 조합은 다음입니다.

  • ToT로 탐색 공간을 설계하고
  • 각 단계(특히 score/select)를 SC로 여러 번 돌려 결정 안정성을 올립니다.

예를 들어 후보 평가가 애매해서 점수가 흔들린다면, score 호출을 5회 수행하고 평균/중앙값으로 안정화합니다. 이때도 CoT는 필요 없습니다. 필요한 것은 구조화된 점수뿐입니다.

하이브리드 의사코드

candidates = generate(context)
scored_samples = [score(context, candidates) for _ in range(k)]
scored = aggregate_scores(scored_samples)  # 평균/중앙값
best = select(scored)
runbook = refine(context, best)

“추론을 숨기면 디버깅이 어려운 것 아닌가”에 대한 해법

CoT를 숨기면 디버깅이 막막해질 수 있습니다. 대신 다음 3가지를 로그로 남기면 운영에 충분합니다.

  1. 입력 요약(정규화): 원문 전체가 아니라 핵심 필드만
  2. 후보/점수/선택 결과: ToT의 산출물
  3. 검증 결과: 스키마 검증, 규칙 검증, 테스트 실행 결과

특히 장애 대응/성능 이슈에서는 “근거”가 서술형 문장보다 체크리스트와 관측 지표로 남는 게 더 유용합니다. 예를 들어 CI 빌드가 느릴 때는 추론 서술보다 캐시 히트율, 레이어 재사용 여부가 핵심입니다. 이런 관점은 Docker 빌드가 느릴 때 BuildKit 캐시 최적화처럼 지표 중심 글을 참고해 평가 기준을 만들 수 있습니다.

실전 체크리스트: SC·ToT 적용 시 흔한 실패와 방지책

1) 출력 포맷이 자꾸 깨진다

  • 해결: JSON 스키마를 강제하고, 파서 실패 시에는 “포맷 복구 전용” 호출을 한 번 더 둡니다.
  • 팁: 포맷 복구 호출은 temperature를 낮게 두고, 입력에 “원문을 그대로 유지하며 JSON만 출력”을 명시합니다.

2) SC를 돌렸는데도 다수결이 의미 없다

  • 원인: 질문이 모호하거나 라벨 정의가 겹칩니다.
  • 해결: 라벨을 줄이고, 각 라벨의 정의/예시를 프롬프트에 포함합니다.

3) ToT가 너무 비싸다

  • 해결: 탐색 폭과 깊이를 제한합니다.
    • 폭: 후보 3~4개
    • 깊이: generate 1회, score 1회, refine 1회
  • 추가: score 단계는 작은 모델로, refine만 큰 모델로 분리하는 것도 효과적입니다.

4) “그럴듯한 런북”은 나오는데 실제로는 틀린다

  • 해결: 점수 기준에 검증 가능성을 넣습니다.
    • 예: “각 단계는 관측 지표 또는 명령어 예시를 포함해야 한다” 같은 하드 룰
  • 추가: 가능한 경우, 모델이 제안한 명령을 실제로 실행하는 자동 검증(샌드박스)을 붙입니다.

결론: CoT를 출력하지 말고, 프로세스를 설계하라

SC와 ToT의 핵심은 “모델이 똑똑해지게”가 아니라, 불확실한 추론을 운영 가능한 의사결정으로 바꾸는 파이프라인입니다.

  • SC는 답을 안정화하는 앙상블
  • ToT는 탐색과 평가를 분리하는 의사결정 트리
  • 둘 다 CoT를 노출하지 않고도 충분히 구현 가능

운영 환경에서 중요한 것은 장황한 추론 텍스트가 아니라, 구조화된 산출물, 점수화 기준, 재현 가능한 로그, 그리고 실패 시의 폴백입니다. 이 네 가지를 갖추면 “추론을 숨기면서도” 품질과 안정성을 동시에 얻을 수 있습니다.