- Published on
CoT 누출 막기 - SCoT·Verifier로 정답률 올리기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙 환경에서 LLM을 붙이다 보면 두 가지 목표가 충돌합니다. 하나는 정답률(reasoning이 길수록 대체로 좋아짐)이고, 다른 하나는 CoT(Chain-of-Thought) 누출 방지(내부 추론을 사용자에게 노출하지 않기)입니다. CoT는 모델의 내부 정책, 프롬프트 구조, 안전 필터 우회 단서까지 포함할 수 있어 보안·컴플라이언스 관점에서 위험합니다. 게다가 장황한 추론을 그대로 반환하면 토큰 비용과 지연도 늘어납니다.
이 글에서는 CoT를 직접 출력하지 않으면서도 성능을 유지하거나 올리는 대표 패턴인 SCoT(Summarized CoT) 와 Verifier(검증기) 조합을 중심으로, 실제 제품 코드에 적용 가능한 설계와 예제를 정리합니다.
왜 CoT를 숨겨야 하나
CoT가 위험한 이유는 단순히 “길어서”가 아닙니다.
- 프롬프트/정책 유출: 시스템 프롬프트의 규칙, 금지사항, 내부 도구 호출 규약이 추론 중에 섞여 나올 수 있습니다.
- 공격 표면 증가: 사용자가 CoT를 바탕으로 다음 턴에서 더 정교한 프롬프트 인젝션을 시도할 수 있습니다.
- 개인정보/비밀 누출: RAG 문서의 원문 일부, 내부 식별자, 운영 데이터가 추론에 포함될 수 있습니다.
- 비용·지연 증가: 긴 reasoning은 토큰을 직접 소모하고, 스트리밍이 아니면 TTFB도 악화됩니다.
요약하면, “모델은 깊게 생각하되, 제품은 안전한 형태로만 보여줘야” 합니다.
핵심 아이디어: CoT는 내부에서만, 출력은 SCoT로
SCoT는 말 그대로 “추론을 하되, 최종 출력에는 요약된 근거만 제공”하는 접근입니다.
- 내부: 모델이 충분히 추론하도록 유도(비공개 reasoning)
- 외부: 사용자에게는 짧은 근거 요약, 결론, 검증 가능한 포인트만 제공
여기서 중요한 점은, SCoT는 단순 요약이 아니라 검증 가능성을 높이는 형태로 만드는 것이 좋습니다.
- 체크리스트 형태
- 가정/전제 나열
- 계산 결과만 제시(중간 과정은 생략)
- 근거 문장에 출처(문서 ID, URL)만 포함
Verifier 패턴: “답을 낸 모델”과 “검증하는 모델”을 분리
Verifier는 생성 모델이 만든 답을 별도의 검증 단계에서 평가/수정하는 패턴입니다.
구성은 보통 다음 중 하나입니다.
- Self-verify: 같은 모델이 다른 프롬프트로 검증
- Cross-model verify: 더 강한 모델 또는 더 저렴한 모델이 검증
- Rule-based verify: 정규식/스키마/도메인 룰로 검증
- Tool-verify: 계산기, DB, 검색, 코드 실행 등 도구로 검증
핵심은 “추론을 보여주는 대신, 검증 절차를 시스템이 수행하고 결과만 사용자에게 제공”하는 것입니다.
권장 아키텍처: Generate - Verify - Finalize
운영에서 가장 깔끔한 형태는 3단계 파이프라인입니다.
- Generate: 답안 후보 생성(내부 reasoning 허용)
- Verify: 후보의 정확성/정합성/안전성 평가
- Finalize: SCoT 형태로 최종 응답 구성
이 구조는 RAG에도 잘 맞습니다. 검색 결과를 붙이면 모델이 그럴듯하게 꾸미는 경향이 있으니, Verifier에서 “인용된 근거가 실제로 근거가 되는지”를 확인하도록 만들 수 있습니다.
관련해서 검색 품질 튜닝은 RAG의 정답률 상한을 올리는 데 중요합니다. 벡터 검색 설정이 흔들리면 Verifier가 아무리 좋아도 회수율 문제로 답이 틀립니다. 필요하면 Pinecone·Milvus 검색품질 튜닝 - HNSW 파라미터도 함께 보세요.
프롬프트 설계: CoT 노출을 막는 안전한 템플릿
MDX 렌더링을 고려해 부등호 문자는 코드로 감싸겠습니다.
Generate 프롬프트(내부)
- 목표: 답을 생성하되, 사용자에게는 reasoning을 출력하지 않도록 지시
- 출력 포맷: 구조화(JSON)로 받아 후처리하기 쉽게
System:
You are a helpful assistant.
Do not reveal chain-of-thought or internal reasoning.
If you need to reason, do it privately.
User:
Question: {{question}}
Return JSON with keys:
- answer: final answer
- short_rationale: 3-5 bullet points with verifiable reasons (no hidden reasoning)
- assumptions: list assumptions (if any)
- citations: list of source ids/urls used
여기서 short_rationale이 SCoT 역할을 합니다. “왜 그런지”를 말하되, 내부 추론을 길게 늘어놓지 말고 검증 가능한 주장만 남깁니다.
Verify 프롬프트(검증기)
Verifier는 “정답인지”만 보는 것이 아니라, 제품 요구사항에 맞게 평가 기준을 명확히 해야 합니다.
System:
You are a strict verifier.
Do not reveal chain-of-thought.
User:
Given the user question and a candidate answer, evaluate:
1) factual correctness
2) internal consistency
3) whether rationale is supported by citations
4) safety/policy compliance
Return JSON:
- verdict: one of [pass, fail, revise]
- issues: list of concrete issues
- revised_answer: if verdict is revise, provide corrected answer
- revised_short_rationale: if revise, provide updated bullet rationale
Verifier가 revise를 반환하면 Finalize 단계에서 교정된 답을 채택합니다.
코드 예제: TypeScript로 SCoT + Verifier 파이프라인
아래는 OpenAI 호환 API 형태를 가정한 의사코드입니다. 핵심은 모델 출력에서 reasoning을 절대 그대로 사용자에게 전달하지 않고, 구조화된 필드만 사용한다는 점입니다.
type Candidate = {
answer: string;
short_rationale: string[];
assumptions?: string[];
citations?: string[];
};
type Verdict = {
verdict: "pass" | "fail" | "revise";
issues: string[];
revised_answer?: string;
revised_short_rationale?: string[];
};
async function callLLM(prompt: string): Promise<string> {
// fetch("/v1/chat/completions", ...)
// return content
return "";
}
function safeJsonParse<T>(s: string): T {
// 운영에서는 zod 같은 스키마 검증 권장
return JSON.parse(s) as T;
}
export async function answerWithScotAndVerifier(question: string) {
const genPrompt = `...${question}...`; // 위 Generate 템플릿
const candidateRaw = await callLLM(genPrompt);
const candidate = safeJsonParse<Candidate>(candidateRaw);
const verifyPrompt = `...${question}...${JSON.stringify(candidate)}...`;
const verdictRaw = await callLLM(verifyPrompt);
const verdict = safeJsonParse<Verdict>(verdictRaw);
if (verdict.verdict === "fail") {
return {
answer: "정확한 답을 확신하기 어려워 추가 정보가 필요합니다.",
rationale: ["검증 단계에서 불일치/근거 부족이 발견되었습니다."],
};
}
if (verdict.verdict === "revise") {
return {
answer: verdict.revised_answer ?? candidate.answer,
rationale: verdict.revised_short_rationale ?? candidate.short_rationale,
};
}
return {
answer: candidate.answer,
rationale: candidate.short_rationale,
};
}
포인트
- 사용자에게 반환하는 것은
answer와rationale뿐입니다. - 모델이 몰래 길게 reasoning을 만들더라도, 그 텍스트를 그대로 출력할 경로를 차단합니다.
- 운영에서는
safeJsonParse단계에 스키마 검증을 넣어 “모델이 이상한 포맷을 내는 경우”를 방어해야 합니다.
Verifier를 강하게 만드는 5가지 체크리스트
1) 스키마 검증(구조적 안정성)
LLM 출력이 JSON처럼 보여도 깨지는 경우가 많습니다. zod 같은 스키마로 강제하면 후속 단계가 안정됩니다.
import { z } from "zod";
const CandidateSchema = z.object({
answer: z.string().min(1),
short_rationale: z.array(z.string().min(1)).min(1).max(6),
assumptions: z.array(z.string()).optional(),
citations: z.array(z.string()).optional(),
});
type Candidate = z.infer<typeof CandidateSchema>;
function parseCandidate(raw: string): Candidate {
const json = JSON.parse(raw);
return CandidateSchema.parse(json);
}
2) 근거-인용 정합성
RAG에서는 특히 “인용이 실제로 답을 지지하는지”가 중요합니다. Verifier에게 다음을 강제하세요.
- 인용이 없으면
revise또는fail - 인용이 질문과 무관하면
fail - 인용이 있는 척 꾸미면
fail
3) 수치/단위/날짜는 도구로 검증
계산은 모델이 자주 틀립니다. 금액, 비율, 날짜 계산은 tool-verify로 빼면 정답률이 크게 오릅니다.
- 환율/세금 계산: 외부 API
- 날짜 차이: 코드 실행
- 통계 요약: 파이썬 런타임
4) 멀티 샘플 + 투표는 “Verifier 비용”이 아니라 “정답률 보험”
Generate를 2~3개 뽑고 Verifier가 최종 선택하는 방식은 실전에서 효과가 좋습니다.
- 후보 간 불일치가 크면
fail로 보내 추가 질문 유도 - 후보가 합의하면
pass확률 상승
5) 실패 시 전략을 제품적으로 정의
Verifier가 fail을 내렸을 때를 미리 설계해야 합니다.
- 추가 질문(필수 입력 누락)
- “확신 불가” 고지 + 가능한 범위 안내
- 사람 검토로 라우팅(고위험 도메인)
이 부분은 운영 장애 대응과도 닮았습니다. 예를 들어 배포 파이프라인에서 캐시가 깨지면 원인을 빠르게 좁히는 체크리스트가 필요하듯이, LLM도 fail을 어떻게 처리할지 체크리스트가 있어야 합니다. CI 관점에서는 GitHub Actions 캐시 안 먹을 때 키·경로·권한 7단계 같은 글의 접근이 그대로 응용됩니다.
CoT 누출을 더 줄이는 운영 팁
로그/트레이싱에서의 누출 차단
LLM 요청/응답을 그대로 로깅하면 내부 프롬프트나 모델 출력이 남습니다.
- 시스템 프롬프트는 해시/버전만 로그
- 사용자 PII 마스킹
- 모델 원문 응답은 샘플링 저장 또는 암호화 저장
- 운영 대시보드에는
answer와rationale만 표시
스트리밍 응답에서의 주의점
스트리밍 중간 토큰에 reasoning이 섞이면 회수하기 어렵습니다.
- 스트리밍은 Finalize 단계 결과만
- Generate/Verify는 비스트리밍으로 내부 처리
비용/지연 최적화
Verifier를 붙이면 비용이 늘어날 수 있습니다. 대신 다음 최적화가 가능합니다.
- 라우팅: 쉬운 질문은 단일 패스로, 어려운 질문만 Verify
- 캐시: 동일 질문/동일 컨텍스트는 결과 캐시
- 모델 계층: Generate는 강한 모델, Verify는 저렴한 모델(또는 반대)
대량 트래픽에서 지연이 문제라면, 서버 스레딩/동시성 튜닝도 같이 봐야 합니다. 자바 백엔드라면 Spring Boot 3.x 가상 스레드로 TPS 2배 튜닝이 LLM 호출이 많은 API에 특히 도움이 됩니다.
평가(Eval): “정답률”을 어떻게 정의하고 올릴 것인가
SCoT·Verifier를 붙였는데도 체감 성능이 안 오르면, 대개 평가가 부정확하거나 목표 함수가 흔들립니다.
권장 지표:
- Accuracy: 정답/오답(골든셋 필요)
- Abstain rate:
fail또는 “추가 정보 필요” 비율 - Hallucination rate: 근거 없는 주장 비율
- Citation precision: 인용이 실제로 지지하는 비율
- Latency p95: Verify 포함 지연
- Cost per answer: 토큰 비용
Verifier는 정답률만 올리는 게 아니라, 보통 abstain을 늘려서 사고를 줄이는 효과도 큽니다. 제품이 허용하는 실패율과 비용을 함께 맞추는 것이 핵심입니다.
정리
- CoT는 정답률에 도움이 되지만, 그대로 노출하면 보안·비용·운영 리스크가 커집니다.
- SCoT는 “추론은 내부에서, 출력은 짧고 검증 가능한 근거로” 바꿔 CoT 누출을 줄입니다.
- Verifier는 생성과 검증을 분리해 정답률과 안정성을 동시에 끌어올립니다.
- 실전에서는 Generate - Verify - Finalize 파이프라인과 스키마 검증, 인용 정합성, tool-verify를 함께 적용하는 것이 효과적입니다.
결국 목표는 “모델이 얼마나 그럴듯하게 말하느냐”가 아니라, 제품이 얼마나 안전하게 정답을 내고 틀릴 때는 틀렸다고 말하느냐입니다. SCoT와 Verifier는 그 균형을 잡는 가장 현실적인 출발점입니다.