Published on

Chain-of-Thought 노출 없이 정확도 올리는 전략

Authors

서로 다른 팀(보안/법무/프로덕트/ML)이 한 가지에 합의하는 경우는 드뭅니다. 그런데 Chain-of-Thought(이하 CoT) 노출은 비교적 쉽게 합의가 됩니다. 노출하면 위험하고, 숨기면 안전해지며, 숨긴다고 성능이 반드시 떨어지는 것도 아닙니다.

문제는 “CoT를 숨기면 정확도가 떨어진다”라는 인식입니다. 실제로는 CoT를 사용자에게 출력하지 않더라도 모델 내부 추론을 유도하거나(비공개), 추론을 대체하는 검증 파이프라인을 붙이거나, 툴 호출을 구조화하는 방식으로 정확도를 체계적으로 끌어올릴 수 있습니다.

이 글은 다음 질문에 답합니다.

  • CoT를 왜 숨겨야 하는가(보안/프롬프트 누출/규정)
  • CoT를 숨긴 채로도 정확도를 올리는 실전 패턴
  • “정답률”을 올리는 데 실제로 효과가 큰 검증·재시도·툴링 설계
  • 운영 환경에서의 장애/지연까지 고려한 아키텍처

CoT를 노출하지 말아야 하는 이유(정확도와 별개)

CoT는 흔히 “모델이 생각하는 과정”처럼 보이지만, 제품 관점에서는 다음 리스크가 더 큽니다.

  1. 프롬프트/정책/시스템 지침 누출
    • 내부 정책(금칙어, 필터링 규칙), 비즈니스 로직, 데이터 소스 힌트가 섞여 나올 수 있습니다.
  2. 공격 표면 확대(프롬프트 인젝션 가속)
    • 공격자는 CoT를 통해 “어떤 지침이 먹히는지”를 빠르게 학습합니다.
  3. 법무/컴플라이언스 리스크
    • 근거 없는 추론이 확신에 찬 문장으로 노출되면, 사용자 오해 및 책임 소재가 커집니다.
  4. 사용자 경험 문제
    • 장황한 추론은 읽기 부담을 만들고, 핵심 답이 묻힙니다.

결론은 간단합니다. CoT는 모델 성능을 위한 내부 메커니즘이지, 사용자에게 제공할 UI가 아닙니다.


목표 재정의: “추론을 보여주지 않고” “정답을 맞히는” 시스템

정확도를 올리는 방법을 한 문장으로 요약하면 이렇습니다.

  • 모델이 답을 만들기 전에 실패 가능성을 줄이는 입력 설계
  • 답을 만든 뒤에 실패를 잡아내는 검증/재시도 설계
  • 모델이 잘 못하는 영역을 툴/규칙/검색으로 대체하는 설계

즉, CoT 대신 검증 가능한 산출물(근거 링크, 인용, 계산 결과, 구조화 JSON) 을 요구하고, 시스템이 이를 자동으로 체크하게 만들면 됩니다.


패턴 1) “CoT 대신 요약된 근거”를 요구하라

사용자에게 추론 과정을 그대로 노출하는 대신, 다음처럼 출력 형식을 바꿉니다.

  • 최종 답변
  • 핵심 근거 3개(짧은 불릿)
  • 출처(문서 링크/레퍼런스)
  • 불확실성(모르는 것은 모른다고)

프롬프트 템플릿(출력 스키마 고정)

당신은 정확한 답을 우선합니다.
- 내부 추론(Chain-of-Thought)은 출력하지 마세요.
- 대신, 사용자가 검증 가능한 '근거 요약'을 3개 이내로 제공하세요.
- 확실하지 않으면 추측하지 말고 필요한 추가 정보를 질문하세요.

출력 형식:
1) 결론: ...
2) 근거 요약:
- ...
- ...
- ...
3) 확인 질문(필요 시): ...

이 방식은 “추론을 숨기면서도” 사용자가 납득할 정보를 제공합니다. 특히 B2B 제품에서 CS 비용을 줄이는 데 효과가 큽니다.


패턴 2) Self-Check(자기검증) + 최소 재시도

CoT를 공개하지 않아도 모델에게 자기검증을 수행하게 한 뒤 최종 답만 출력하게 할 수 있습니다.

핵심은 “검증 단계는 내부적으로 수행하고, 사용자에게는 결과만 보여준다”입니다.

2단계 호출 예시(애플리케이션 레벨)

  • 1차: 답 생성
  • 2차: 답 검증(사실성/형식/금칙 위반/모호성)
  • 실패 시: 조건을 강화해 1회만 재생성
// TypeScript 예시: 2-pass generate -> verify

type Draft = {
  answer: string;
  citations?: string[];
};

type Verdict = {
  ok: boolean;
  issues: string[];
  fixHint?: string;
};

async function generateDraft(question: string): Promise<Draft> {
  // LLM call 1
  return {
    answer: "...",
    citations: ["https://..."]
  };
}

async function verifyDraft(question: string, draft: Draft): Promise<Verdict> {
  // LLM call 2 (or rule-based + LLM)
  return {
    ok: true,
    issues: []
  };
}

export async function answer(question: string): Promise<string> {
  const draft = await generateDraft(question);
  const verdict = await verifyDraft(question, draft);

  if (verdict.ok) return draft.answer;

  // 최소 재시도 1회: 비용/지연 폭주 방지
  const revised = await generateDraft(
    question + "\n\n제약: " + verdict.fixHint
  );
  return revised.answer;
}

운영에서 중요한 포인트는 “재시도 무한루프”를 막는 것입니다. 에이전트/툴 호출이 섞이면 폭주하기 쉬우니, 상한선을 강제하세요. 이 주제는 LangChain 에이전트 무한루프·툴폭주 차단 8단계에서 다룬 방식과 같은 결로 접근하면 좋습니다.


패턴 3) “정답을 만들지 말고” 먼저 질의 명세를 구조화하라

정확도가 낮은 이유 중 상당수는 모델이 문제를 잘못 이해해서입니다. 해결책은 간단합니다.

  • 사용자의 자연어 질문을 바로 답하지 말고
  • 먼저 질의 명세(JSON) 로 변환한 뒤
  • 그 명세를 기반으로 검색/계산/정책 적용을 수행합니다.

예: 질문을 Intent JSON으로 변환

{
  "intent": "compare",
  "topic": "vector database",
  "constraints": {
    "budget": "low",
    "scale": "medium",
    "must_have": ["hybrid search"],
    "region": "ap-northeast-2"
  },
  "output": {
    "format": "table",
    "include_risks": true
  }
}

이때 스키마가 조금만 흔들려도 운영에서 장애가 납니다. 툴 호출/JSON 스키마 오류를 다룰 때는 스키마를 강제하고, 실패 시 복구 경로를 두세요. 관련해서는 Claude Tool Use 400 에러 - JSON 스키마 해결법 같은 접근(스키마 엄격화, 에러 메시지 기반 재시도)이 그대로 적용됩니다.


패턴 4) RAG는 “검색”보다 “인용 강제 + 커버리지 체크”가 핵심

RAG를 붙였는데도 환각이 줄지 않는 경우가 많습니다. 이유는 보통 둘 중 하나입니다.

  • 검색 결과를 모델이 무시한다
  • 검색 결과가 질문을 커버하지 못한다

해결은 “검색”이 아니라 인용(citation)을 강제하고, 커버리지(coverage)를 채점하는 것입니다.

프롬프트: 인용 없는 문장은 금지

규칙:
- 제공된 컨텍스트에 없는 사실은 단정하지 마세요.
- 모든 핵심 주장 문장 끝에 [source:N] 형태로 인용을 붙이세요.
- 인용을 붙일 수 없으면 "확인 불가"라고 쓰세요.

커버리지 체크(간단한 룰 기반)

  • 답변의 각 문장에 [source: 가 포함되는지 검사
  • 포함되지 않으면 “추측”으로 간주하고 재생성

이렇게 하면 CoT 없이도 “근거 중심”으로 모델을 몰아갈 수 있습니다.


패턴 5) 함수 호출/툴 사용은 “작게, 결정적으로”

정확도를 올리려다가 오히려 망가지는 지점이 툴 사용입니다.

  • 툴이 많을수록 선택 오류 증가
  • 네트워크 실패/타임아웃 증가
  • 결과 파싱 실패 증가

따라서 툴은 적게, 그리고 출력 계약을 강하게 가져가야 합니다.

예: 계산은 모델이 아니라 툴로

# Python 예시: 수치 계산은 코드로, 모델은 해설만

def calc_compound(principal: float, rate: float, years: int) -> float:
    return principal * ((1 + rate) ** years)

principal = 1000
rate = 0.07
years = 10
result = calc_compound(principal, rate, years)
print(result)

모델에게는 result를 주고 “해석”만 시키면 환각 영역이 줄어듭니다.


패턴 6) 불확실성 라벨링: “모르는 건 모른다”를 시스템 요구사항으로

CoT를 숨기면 사용자는 모델의 고민을 볼 수 없으니, 대신 불확실성을 명시해야 합니다.

  • 확실: 단정
  • 가능: 조건부 표현
  • 확인 필요: 질문/추가 데이터 요청

이 규칙을 출력 포맷에 포함하면, 환각을 “정확도 저하”가 아니라 “확인 필요”로 전환할 수 있습니다.

출력 포맷 예시

결론: ...
확실한 근거: ...
가정/조건: ...
확인 필요: ...

패턴 7) 프로덕션에서의 정확도는 “지연/실패”와 함께 관리해야 한다

정확도를 올리는 기능(2-pass, RAG, 툴 호출, 재시도)은 대개 지연과 실패율을 올립니다.

  • 재시도는 504를 만들고
  • 스케일-투-제로 환경은 콜드스타트로 지연이 튀고
  • 외부 벡터DB/검색 API는 간헐 장애를 만듭니다.

따라서 “정확도 향상 파이프라인”은 다음을 같이 설계해야 합니다.

  • 타임아웃 예산(예: 전체 8초, 검색 2초, 생성 4초, 검증 2초)
  • 서킷 브레이커(검색 장애 시 안전한 축약 답변)
  • 캐시(같은 질문/같은 문서에 대한 재검색 방지)

서빙 레이어에서 503이나 콜드스타트 지연이 있다면, 정확도 이전에 안정성을 먼저 잡아야 합니다. K8s 기반 서빙이라면 KServe LLM 서빙 503·스케일0 지연 해결법 같은 체크리스트가 그대로 도움이 됩니다. 서버리스에서 타임아웃이 잦다면 Cloud Run 504 Timeout 원인·해결 9가지를 함께 참고해 “정확도 기능”이 장애를 부르지 않게 조정하세요.


운영 체크리스트: CoT 비노출 정확도 개선 10항

  1. 사용자 출력에서 CoT/내부 정책/시스템 프롬프트가 절대 나오지 않게 규칙화
  2. 출력 포맷 고정(결론/근거/출처/불확실성)
  3. 질의 명세(JSON) 단계로 입력을 정규화
  4. RAG는 인용 강제 + 인용 커버리지 검사
  5. 2-pass(생성/검증) 적용, 재시도는 1회로 제한
  6. 계산/정렬/필터링은 툴로, 모델은 해설로
  7. 금칙/정책 위반은 룰 기반으로 1차 차단
  8. 타임아웃 예산을 단계별로 배분
  9. 장애 시 축약 답변으로 degrade(예: “확인 불가/추가 질문”)
  10. 오프라인 평가셋으로 회귀 테스트(프롬프트 변경 시 필수)

마무리: CoT를 숨기는 것은 “성능 포기”가 아니라 “제품화”다

CoT 노출 없이 정확도를 올리는 핵심은, 모델의 장황한 추론을 사용자에게 보여주는 대신 검증 가능한 산출물자동 검증 파이프라인으로 품질을 끌어올리는 것입니다.

정리하면 다음 순서가 가장 재현성이 높습니다.

  • 출력 형식 고정(근거/출처/불확실성)
  • 질의 명세화(JSON)
  • RAG 인용 강제
  • 2-pass 검증 + 제한적 재시도
  • 툴 사용 최소화 + 계약 강제
  • 운영 타임아웃/장애 대응까지 포함한 설계

이 조합이면 CoT를 숨겨도, 오히려 운영 품질은 더 좋아지는 경우가 많습니다.