- Published on
o1/4o에서 CoT 숨기고 정확도 올리는 SC+Verifier
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
o1/4o 계열 모델을 제품에 붙일 때 가장 난감한 지점은 두 가지가 동시에 온다는 점입니다. 첫째, 정확도를 높이려고 추론 과정을 길게 유도하면 사용자에게 내부 사고 과정(Chain-of-Thought, 이하 CoT)이 노출될 수 있습니다. 둘째, CoT 노출을 막으려고 답만 짧게 받으면, 복잡한 문제에서 정답률이 흔들립니다.
이 글은 이 딜레마를 “CoT는 숨기되 정확도는 올리는” 방향으로 해결하는 패턴을 다룹니다. 핵심은 다음 조합입니다.
- SC(Self-Consistency): 여러 번 독립적으로 풀고 다수결 또는 점수 기반으로 합의안을 뽑는다.
- Verifier(검증기): 후보 답을 별도 모델/별도 프롬프트로 검증하고, 실패 시 재시도 또는 재탐색한다.
중요한 포인트는 “추론을 더 많이 하되, 사용자에게는 노출하지 않는다”입니다. 이를 위해 모델 출력은 최종 답과 최소한의 근거(혹은 근거 없음)로 제한하고, 검증/합의는 시스템 내부에서만 수행합니다.
왜 SC+Verifier가 CoT 없이도 먹히는가
CoT 노출 없이도 추론은 가능하다
모델이 추론을 못 하는 게 아니라 “추론 텍스트를 출력하지 않게” 만드는 것이 목적입니다. 즉, 내부적으로는 다양한 경로로 답을 만들어도, 외부 출력은 최종 결과만 내보내면 됩니다.
실무적으로는 다음 두 가지가 중요합니다.
- 출력 스키마를 강하게 고정한다(예: JSON only).
- “설명하지 말고 답만”을 강하게 요구한다.
이렇게 하면 모델이 장황한 CoT를 쏟아내기 어렵고, 설령 내부적으로 긴 추론을 하더라도 결과만 나오게 됩니다.
SC(Self-Consistency)의 역할: 분산을 평균내기
LLM은 같은 문제를 여러 번 풀면 경로가 달라집니다. 특히 애매하거나 함정이 있는 문제에서 그 분산이 커지는데, SC는 이 분산을 이용해 안정적인 답을 고릅니다.
- 장점: 한 번의 “대박 추론”에 의존하지 않고, 평균적으로 정답률이 올라간다.
- 단점: 비용과 지연이 증가한다.
Verifier의 역할: 합의안의 취약점을 찌르기
SC로 다수결을 해도 “다수가 틀리는” 케이스가 있습니다. Verifier는 후보 답을 대상으로 반례를 찾거나, 제약조건을 체크하거나, 계산을 재검증해 실패를 걸러냅니다.
Verifier는 다음 두 접근 중 하나로 설계합니다.
- 규칙 기반 검증(가능하면 최우선): 타입/형식/제약조건/수학 검산/도메인 룰 체크
- LLM 기반 검증: 후보 답을 입력으로 받아 “정답/오답 및 이유(내부용)”를 판단
LLM 기반 검증을 쓰더라도, 검증기의 출력은 사용자에게 나가지 않게 합니다.
설계 패턴: Generate K개 후보 + Score/Verify + Select
전체 파이프라인은 다음 단계로 정리됩니다.
- Candidate 생성: 동일 문제를 K번 풀어 후보를 만든다(SC)
- 정규화: 후보를 동일 포맷으로 파싱/정규화한다
- 1차 선택: 다수결 또는 간단한 휴리스틱으로 상위 N개를 고른다
- Verifier 검증: 상위 후보를 검증하고 통과한 답을 선택한다
- 실패 시 재시도: K를 늘리거나, 프롬프트를 바꾸거나, 별도 도구(계산기/DB)를 붙인다
이때 “CoT 숨김”을 위해, 후보 생성 단계부터 출력은 엄격히 제한합니다.
프롬프트 전략: CoT 대신 구조화된 결과만 받기
아래는 후보 생성 프롬프트 예시입니다. 핵심은 설명 금지와 스키마 고정입니다.
System:
너는 정확한 문제 해결사다. 내부 추론은 하되, 절대 추론 과정을 출력하지 마라.
출력은 반드시 JSON 하나만 반환한다.
User:
문제: {question}
출력 스키마:
{
"final": "최종 답",
"confidence": 0.0,
"notes": "검증에 도움이 되는 매우 짧은 힌트(선택). 추론 과정 금지."
}
규칙:
- reasoning, chain-of-thought, step-by-step 같은 형식의 설명을 쓰지 마라.
- JSON 외 텍스트를 출력하지 마라.
notes는 내부 검증에 도움이 되는 최소 단서만 허용하는 장치입니다. 예를 들어 “사용한 공식 이름” 정도는 허용하되, 전개 과정은 금지합니다.
코드 예제: Responses API로 SC+Verifier 구현(파이썬)
아래 예시는 다음을 구현합니다.
- o1/4o로 후보 K개 생성
- 후보를 파싱해 다수결(또는 confidence 가중)로 1차 선택
- 별도 Verifier 호출로 최종 확정
- 실패하면 재시도
네트워크 타임아웃/재시도는 실무에서 필수입니다. 특히 대량 병렬 호출 시 504가 발생할 수 있으니, 운영 환경에서는 타임아웃과 백오프를 꼭 넣으세요. 관련해서는 OpenAI Responses API 504 Timeout 재현·해결도 함께 참고하면 좋습니다.
import json
import random
import time
from collections import Counter
from typing import Any, Dict, List, Optional, Tuple
from openai import OpenAI
client = OpenAI()
MODEL_SOLVER = "gpt-4o" # 또는 o1 계열을 사용
MODEL_VERIFIER = "gpt-4o" # 검증기는 동일 모델 또는 더 저렴한 모델로 분리 가능
def call_responses_json(model: str, system: str, user: str, timeout_s: float = 30.0) -> Dict[str, Any]:
# SDK/런타임에 따라 timeout 옵션은 다를 수 있음. 여기서는 개념 예시.
resp = client.responses.create(
model=model,
input=[
{"role": "system", "content": system},
{"role": "user", "content": user},
],
temperature=0.8,
)
# Responses API는 output 텍스트를 여러 조각으로 줄 수 있음. 단순화된 예시.
text = resp.output_text
return json.loads(text)
def generate_candidates(question: str, k: int) -> List[Dict[str, Any]]:
system = (
"너는 정확한 문제 해결사다. 내부 추론은 하되, 절대 추론 과정을 출력하지 마라. "
"출력은 반드시 JSON 하나만 반환한다."
)
user = (
f"문제: {question}\n\n"
"출력 스키마:\n"
"{\n"
" \"final\": \"최종 답\",\n"
" \"confidence\": 0.0,\n"
" \"notes\": \"검증에 도움이 되는 매우 짧은 힌트(선택). 추론 과정 금지.\"\n"
"}\n\n"
"규칙:\n"
"- reasoning, chain-of-thought, step-by-step 같은 형식의 설명을 쓰지 마라.\n"
"- JSON 외 텍스트를 출력하지 마라."
)
out: List[Dict[str, Any]] = []
for _ in range(k):
out.append(call_responses_json(MODEL_SOLVER, system, user))
return out
def pick_by_self_consistency(candidates: List[Dict[str, Any]]) -> Tuple[str, List[Dict[str, Any]]]:
finals = [c.get("final", "").strip() for c in candidates]
counts = Counter(finals)
best_final, _ = counts.most_common(1)[0]
# best_final과 동일한 후보들을 모아 verifier에 넘기거나, confidence 상위만 추릴 수 있음
same = [c for c in candidates if c.get("final", "").strip() == best_final]
return best_final, same
def verify_answer(question: str, final_answer: str) -> Dict[str, Any]:
system = (
"너는 엄격한 검증기다. 내부 추론은 하되, 절대 추론 과정을 출력하지 마라. "
"출력은 반드시 JSON 하나만 반환한다."
)
user = (
"다음 문제와 답을 검증하라.\n"
f"문제: {question}\n"
f"답: {final_answer}\n\n"
"출력 스키마:\n"
"{\n"
" \"verdict\": \"pass|fail\",\n"
" \"reason\": \"아주 짧은 실패 사유 또는 통과 근거(추론 과정 금지)\",\n"
" \"fix\": \"fail이면 올바른 답만 제시, pass면 빈 문자열\"\n"
"}\n\n"
"규칙:\n"
"- step-by-step 설명 금지\n"
"- JSON 외 텍스트 금지"
)
return call_responses_json(MODEL_VERIFIER, system, user)
def solve_with_sc_verifier(question: str, k: int = 5, max_rounds: int = 2) -> str:
for rnd in range(max_rounds):
candidates = generate_candidates(question, k=k)
best_final, _ = pick_by_self_consistency(candidates)
verdict = verify_answer(question, best_final)
if verdict.get("verdict") == "pass":
return best_final
# verifier가 수정 답을 주면 그걸 채택하거나, 재탐색 트리거로 사용
fix = (verdict.get("fix") or "").strip()
if fix:
# fix도 다시 한 번 verifier로 재검증하는 방어가 유용
verdict2 = verify_answer(question, fix)
if verdict2.get("verdict") == "pass":
return fix
# 재시도: k를 늘리거나 temperature를 조정하는 전략도 가능
k = int(k * 1.5) + 1
time.sleep(0.2 + random.random() * 0.3)
# 여기까지 오면 실패. 제품에서는 fallback(사람에게 escalation, 도구 호출 등) 고려
raise RuntimeError("Failed to solve with SC+Verifier")
이 구조의 장점은 명확합니다.
- 사용자에게는
final만 보여주면 되므로 CoT가 노출되지 않는다. - 내부적으로는 K번 샘플링과 검증으로 정확도를 올릴 수 있다.
Verifier를 더 강하게 만드는 법: 반례 탐색과 제약 체크
Verifier가 단순히 “맞다/틀리다”만 말하면 효용이 떨어집니다. 다음을 추가하면 성능이 체감됩니다.
1) 반례를 찾는 검증 질문
- “이 답이 틀리게 되는 입력/조건이 존재하는가?”
- “문제의 모든 제약을 만족하는가?”
- “단위/범위/경계값에서 깨지지 않는가?”
Verifier 프롬프트에 이런 체크리스트를 넣되, 출력은 여전히 짧은 JSON으로 제한합니다.
2) 규칙 기반 검증을 먼저 적용
가능한 도메인에서는 LLM보다 규칙 기반이 더 싸고 정확합니다.
- 수학/통계: 파이썬으로 재계산
- 코드: 타입체크, 테스트 실행, 린트
- 데이터: 스키마 검증
예를 들어 파케이 스키마 불일치 같은 문제는 LLM이 “그럴듯한 말”을 할 수는 있어도, 실제 원인은 스키마/타입에 있습니다. 이런 류는 규칙 기반을 우선하고, LLM은 보조로 두는 편이 안전합니다. 데이터 파이프라인 이슈 사례는 Python ArrowInvalid - Parquet 스키마 불일치 해결법 같은 글을 함께 보면 연결이 잘 됩니다.
운영 관점 체크리스트: 비용, 지연, 장애
SC+Verifier는 호출 수가 늘어나므로 운영 이슈가 바로 튀어나옵니다.
비용과 지연
- 비용은 대략
K + verifier_calls배로 증가합니다. - 지연은 병렬화를 하면 줄일 수 있지만, 레이트리밋/타임아웃이 증가합니다.
대량 트래픽에서는 타임아웃과 재시도가 필수입니다. http 클라이언트 레벨 재시도 설계는 Python httpx ReadTimeout·ConnectError 재시도 설계처럼 “어떤 에러에, 어떤 백오프로, 몇 번까지”를 명시적으로 정하는 방식이 안정적입니다.
캐싱
- 동일 질문은 후보 생성 결과를 캐시해 K 호출을 줄일 수 있습니다.
- 단, 확률적 모델 출력이므로 캐시 키는
question + system_prompt_version + model + temperature를 포함하는 편이 안전합니다.
로깅(단, CoT 로깅 금지)
- 사용자에게 CoT를 숨기더라도, 내부 로깅에 CoT가 남으면 보안/정책 이슈가 됩니다.
- 따라서 후보/검증 로그는
final,confidence,verdict,reason(짧게)정도로 제한하세요.
SC+Verifier 변형 3가지
1) Two-stage SC: 먼저 싸게 많이, 그다음 비싸게 적게
- 1단계: 저렴한 모델로 K를 크게 샘플링
- 2단계: 상위 후보 N개만 o1/4o로 재검증
비용을 크게 줄이면서도 정확도를 유지하는 전형적인 구조입니다.
2) Debate 대신 “evidence checklist”
토론형(debate)은 출력이 길어지고 CoT가 새기 쉬워 운영 난도가 올라갑니다. 대신 Verifier가 체크리스트 기반으로 “통과/실패”만 내리게 하면 CoT 노출 위험이 낮습니다.
3) Tool-augmented Verifier
Verifier가 계산/검색/DB를 직접 호출하게 만들면 훨씬 강해집니다. 예를 들어 숫자 계산은 파이썬으로, 쿼리 검증은 DB로 돌리고, LLM은 최종 판정만 하게 하면 됩니다.
결론
o1/4o에서 CoT를 숨기면서 정확도를 올리는 가장 실전적인 방법 중 하나가 SC(Self-Consistency)와 Verifier(검증기) 조합입니다. 핵심은 “모델이 생각을 더 하도록 만들되, 출력은 구조화된 최종 답만 허용”하고, 내부적으로는 다중 후보와 엄격한 검증으로 실패를 줄이는 것입니다.
정리하면 다음 순서로 적용하는 것이 좋습니다.
- 출력 스키마 고정으로 CoT 노출을 원천적으로 줄인다
- SC로 후보를 여러 개 만들고 합의안을 고른다
- Verifier로 반례/제약을 체크해 통과한 답만 사용자에게 반환한다
- 운영에서는 타임아웃/재시도/캐싱/로깅 정책을 함께 설계한다
이 패턴을 기본 골격으로 삼고, 도메인에 맞게 규칙 기반 검증과 도구 호출을 추가하면 “짧게 답하지만 틀리지 않는” 시스템에 가까워집니다.