Published on

CoT 노출 없이 추론품질↑ - Self-Consistency+Verifier

Authors

서론

LLM 기반 기능을 제품에 넣다 보면 곧바로 부딪히는 딜레마가 있습니다. 추론 품질을 높이려면 모델이 중간 추론(Chain-of-Thought, 이하 CoT)을 길게 쓰는 편이 유리한데, 그 CoT를 그대로 사용자에게 노출하면 보안·정책·UX 측면에서 문제가 생깁니다. 예를 들어 내부 규칙, 민감한 힌트, 취약한 논리 전개가 그대로 드러나거나, 사용자에게 불필요하게 장황한 답변이 나가기도 합니다.

이 글에서는 CoT를 외부로 노출하지 않으면서도 품질을 올리는 대표 조합인 Self-Consistency(다중 샘플링 후 다수결)와 Verifier(검증자 모델/규칙으로 후보를 평가)를 결합해, 실제 서비스에 적용 가능한 형태로 정리합니다. 핵심은 다음 한 줄입니다.

  • 생성은 다양하게, 선택은 엄격하게, 출력은 짧고 근거 중심으로

또한 운영 중 API 오류나 스키마 문제로 디버깅이 막힐 때를 대비해, 요청 구조를 안정화하는 팁도 함께 다룹니다. 관련해서는 OpenAI Responses API 400 invalid_request_error 해결 글도 같이 보면 좋습니다.

왜 CoT를 숨기면서도 품질을 올려야 하나

CoT 노출의 실무 리스크

  1. 정책/보안

    • 내부 프롬프트, 시스템 규칙, 숨겨진 컨텍스트가 역으로 추정될 수 있습니다.
    • 민감 정보가 추론 과정에 섞여 나갈 수 있습니다.
  2. 신뢰성

    • CoT는 정답의 “증거”가 아닙니다. 그럴듯한 서술이 오히려 신뢰를 왜곡할 수 있습니다.
  3. UX/비용

    • 토큰이 늘고, 응답이 장황해지며, 지연이 커집니다.

그렇다고 짧게만 답하면 품질이 떨어지는 이유

짧은 답변은 종종 다음을 놓칩니다.

  • 모호한 요구사항에서의 가정 정리
  • 계산/제약 조건의 점검
  • 반례 탐색 및 오류 수정

따라서 “내부적으로는 많이 생각하되, 외부적으로는 간결하게 답하는” 구조가 필요합니다.

Self-Consistency: 다중 샘플링으로 정답 후보를 모은다

Self-Consistency는 간단히 말해 같은 문제를 여러 번 풀게 한 뒤, 가장 자주 등장하는 결론(또는 가장 일관된 결론)을 채택하는 방식입니다.

언제 효과적인가

  • 수학/논리/규칙 기반 문제
  • 정답 공간이 좁고, 오답이 분산되는 문제
  • 한 번의 샘플링에서 “운 좋게” 맞히는 게 아니라 안정적으로 맞혀야 하는 문제

설계 포인트

  • temperature를 올려 다양성을 확보합니다.
  • n(샘플 수)을 늘릴수록 비용은 증가하지만 성능이 올라갑니다.
  • “최종 답”을 구조화해 비교 가능하게 만들어야 합니다.

예: 모델에게 최종 답을 final_answer 필드로만 내게 하고, 그 외는 내부적으로만 사용하도록 지시합니다.

Verifier: 후보를 채점해 최종 선택을 한다

Self-Consistency가 “후보 생성”이라면, Verifier는 “후보 선별”입니다. Verifier는 별도 모델일 수도 있고, 규칙 기반 검사기일 수도 있습니다.

Verifier의 유형

  1. LLM-as-a-judge

    • 후보 답안을 보고 제약조건 만족 여부, 모순, 근거 적합성 등을 평가
  2. 규칙/정적 검사

    • JSON 스키마 검증, 정규식, 타입 체크, 금칙어 필터
  3. 실행 기반 검증

    • 코드면 유닛 테스트, SQL이면 EXPLAIN/샌드박스 실행, 수학이면 계산기/심볼릭 툴

실무에서는 1과 2를 먼저 붙이고, 3은 비용 대비 효과가 큰 영역부터 단계적으로 도입하는 편이 안전합니다.

Self-Consistency + Verifier 결합 패턴

결합은 다음 파이프라인으로 정리됩니다.

  1. Generator가 k개의 후보를 생성
  2. 후보를 정규화(normalize)하여 비교/검증 가능한 형태로 변환
  3. Verifier가 각 후보를 채점
  4. 최고 점수 후보를 선택
  5. 사용자에게는 “짧은 최종 답 + 간단한 근거 요약”만 출력

이때 중요한 점은 “CoT를 아예 만들지 말라”가 아니라, “CoT는 내부에서만 사용하고 외부로는 내보내지 말라”입니다.

프롬프트 전략: CoT 대신 구조화된 산출물 강제

CoT를 직접 출력하지 않게 하려면, 출력 형식을 강하게 제한하는 것이 효과적입니다.

  • 최종 답만 출력하도록 요구
  • 근거는 2~5줄 요약으로 제한
  • 검증 가능한 체크리스트 형태로 근거를 작성

예를 들어 다음처럼 요구합니다.

  • final_answer: 사용자가 원하는 최종 답
  • rationale_summary: 3줄 이내의 근거 요약(과정 상세 금지)
  • assumptions: 가정 목록

이렇게 하면 모델이 내부적으로는 추론하되, 외부로는 “요약된 근거”만 내는 형태가 됩니다.

구현 예제: Node.js로 Self-Consistency + Verifier

아래 예제는 개념을 보여주기 위한 최소 구현입니다.

  • Generator: 동일 프롬프트로 k번 호출
  • Verifier: 간단한 규칙 + LLM 채점(옵션)
  • 출력: 최종 답만 사용자에게 제공

주의: 코드 블록 내의 부등호는 MDX 빌드에 안전하지만, 본문 일반 텍스트에서는 > 또는 <가 노출되지 않도록 했습니다.

// self_consistency_verifier.js
// 실행: node self_consistency_verifier.js

import OpenAI from "openai";

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

const TASK = `
사용자 질문에 답하라.
출력은 JSON 하나로만.
- final_answer: 최종 답
- rationale_summary: 근거 요약 3줄 이내 (상세 추론 과정 금지)
- assumptions: 가정 배열
`;

async function generateCandidate(userQuestion, temperature) {
  const input = `${TASK}\n질문: ${userQuestion}`;

  const res = await client.responses.create({
    model: "gpt-4.1-mini",
    input,
    temperature,
  });

  // 예제 단순화를 위해 text를 JSON으로 파싱한다고 가정
  const text = res.output_text;
  return JSON.parse(text);
}

function ruleBasedVerify(candidate) {
  // 매우 단순한 예: 필드 존재, 길이 제한
  if (!candidate?.final_answer) return { ok: false, score: 0, reason: "no final_answer" };
  if (!candidate?.rationale_summary) return { ok: false, score: 0, reason: "no rationale_summary" };

  const len = candidate.rationale_summary.split("\n").filter(Boolean).length;
  if (len > 3) return { ok: false, score: 0.2, reason: "rationale too long" };

  return { ok: true, score: 0.6, reason: "passes basic rules" };
}

async function llmVerify(userQuestion, candidate) {
  // LLM을 판정자로 쓰는 경우. 후보들 중 무엇이 더 적합한지 점수화.
  const judgePrompt = `
너는 검증자다.
질문과 후보 답안을 보고, 정확성/일관성/가정의 타당성을 평가해라.
상세 추론은 쓰지 말고, score 0~1과 짧은 comment만 JSON으로 출력.

질문: ${userQuestion}
후보: ${JSON.stringify(candidate)}
`;

  const res = await client.responses.create({
    model: "gpt-4.1-mini",
    input: judgePrompt,
    temperature: 0,
  });

  const judged = JSON.parse(res.output_text);
  return judged; // { score: 0.0~1.0, comment: "..." }
}

async function solve(userQuestion, k = 7) {
  const temps = Array.from({ length: k }, (_, i) => 0.6 + i * 0.1);

  const candidates = [];
  for (let i = 0; i < k; i++) {
    const c = await generateCandidate(userQuestion, temps[i]);
    candidates.push(c);
  }

  let best = null;
  for (const c of candidates) {
    const rule = ruleBasedVerify(c);
    if (!rule.ok) continue;

    const judge = await llmVerify(userQuestion, c);
    const total = 0.3 * rule.score + 0.7 * judge.score;

    if (!best || total > best.total) {
      best = { candidate: c, total, judge, rule };
    }
  }

  if (!best) {
    return {
      final_answer: "답을 생성했지만 검증을 통과한 후보가 없습니다.",
      rationale_summary: "형식 또는 제약 조건 위반으로 재시도가 필요합니다.",
      assumptions: [],
    };
  }

  return best.candidate;
}

const question = "Self-Consistency와 Verifier를 결합하면 어떤 장점이 있나?";
solve(question).then((out) => {
  console.log(JSON.stringify(out, null, 2));
});

위 구현은 단순하지만, 핵심 구조는 그대로입니다.

  • Generator는 다양하게 만든다
  • Verifier는 제약을 강하게 건다
  • 최종 출력은 구조화된 짧은 답만 준다

운영에서 자주 생기는 실패 모드와 대응

1) 후보 답안이 서로 다르게 “그럴듯”할 때

Self-Consistency만 쓰면 다수결이 항상 정답을 보장하지 않습니다. 특히 정답 공간이 넓거나, 모델이 특정 편향된 오답을 반복 생성하면 다수결이 오히려 나빠질 수 있습니다.

대응:

  • Verifier를 반드시 붙여 “다수”가 아니라 “제약 만족”을 우선
  • 후보 간 차이를 줄이는 대신, 검증 신호를 늘립니다(규칙 검사, 지식 베이스 대조, 실행 기반 테스트)

2) Verifier가 틀린 답을 높게 점수 줄 때

LLM 판정자는 때때로 자신감 있게 잘못된 채점을 합니다.

대응:

  • Verifier 프롬프트에 “모르면 낮은 점수” 규칙을 명시
  • 채점 근거를 짧게 코멘트로 남겨 관측 가능하게 만들기
  • 가능하면 규칙/실행 기반 검증을 섞어 LLM 단독 판정을 피하기

3) 비용과 지연이 급증할 때

k개 생성 후 검증까지 하면 호출 수가 k + k로 늘어납니다.

대응:

  • 2단계 필터링: 규칙 검사로 먼저 탈락시키고, 상위 m개만 LLM Verifier
  • 캐시: 동일 질문, 동일 컨텍스트면 후보/채점 결과 캐싱
  • 트래픽 구간별로 k를 동적으로 조절

이런 “단계적 게이트”는 대규모 CI 파이프라인에서 병목을 줄이는 방식과 유사합니다. 모노레포에서 병목을 진단하듯, 생성과 검증 단계별 비용을 계측해 병목을 찾아야 합니다. 참고로 대형 저장소 병목을 다루는 글로 대형 모노레포에서 Git LFS가 느려질 때 진단법도 같은 접근법(관측 후 병목 제거)을 제공합니다.

CoT 미노출을 위한 출력 정책: “근거 요약”만 제공

사용자에게는 다음만 제공하는 패턴이 안전합니다.

  • 결론
  • 가정(필요 시)
  • 검증 가능한 짧은 근거 요약
  • 추가 확인 질문(불확실할 때)

반대로 다음은 피하는 편이 좋습니다.

  • 상세한 단계별 추론
  • 내부 정책/규칙을 그대로 드러내는 설명
  • 모델이 스스로 만든 출처를 확정적으로 말하는 행위

즉, “추론 과정”이 아니라 “검증 가능한 결과물”을 중심으로 답하게 해야 합니다.

실전 적용 체크리스트

  1. 출력 스키마부터 고정

    • final_answerrationale_summary 같은 필드를 강제
  2. Self-Consistency는 다양성 확보가 목적

    • temperature를 올리고 k를 적절히 설정
  3. Verifier는 다층 방어

    • 규칙 기반 검사로 1차 필터
    • LLM 판정자는 2차
    • 가능하면 실행 기반 검증으로 3차
  4. 관측 가능성

    • 후보별 점수, 탈락 사유, 평균 지연을 로깅
  5. 장애 대응

결론

CoT를 사용자에게 노출하지 않으면서도 추론 품질을 올리려면, “한 번에 잘 맞히기” 전략을 버리고 “여러 번 생성하고 검증으로 고르기”로 바꾸는 것이 가장 현실적인 해법입니다.

  • Self-Consistency로 후보의 다양성과 안정성을 확보하고
  • Verifier로 제약 조건과 정확성을 강하게 걸러내며
  • 사용자에게는 간결한 최종 답과 짧은 근거 요약만 제공

이 조합은 모델이 내부적으로는 충분히 사고하도록 하면서도, 제품 표면에서는 안전하고 일관된 응답을 만들 수 있는 실전 패턴입니다.