Published on

CoT 노출 없이 추론 강화 - SCR·RAG 결합

Authors

서버나 제품 환경에서 LLM 추론력을 끌어올릴 때 가장 흔한 딜레마는 이겁니다.

  • 추론을 강하게 만들려면 중간 사고 과정을 길게 쓰게 되는데, 그게 그대로 노출되면 보안·프롬프트 유출·규정 준수 리스크가 생깁니다.
  • 반대로 중간 사고를 숨기거나 짧게 만들면 정답률이 떨어지거나, 근거가 빈약해져 운영 이슈가 늘어납니다.

이 글은 CoT(Chain-of-Thought)를 사용자 응답에 노출하지 않는 것을 전제로, SCR(Self-Consistency Reasoning)RAG(Retrieval-Augmented Generation)를 결합해 정확도, 일관성, 근거성, 운영 안정성을 동시에 확보하는 방법을 다룹니다.

이미 CoT 비노출 프롬프트 테크닉이 궁금하다면 먼저 아래 글을 읽고 오면 연결이 쉽습니다.

왜 CoT를 숨겨야 하나

CoT를 그대로 노출하면 다음 문제가 실제로 자주 터집니다.

  1. 프롬프트 인젝션 표면 증가: 내부 규칙, 시스템 프롬프트, 툴 호출 힌트가 사고 과정에 섞여 나갈 수 있습니다.
  2. 개인정보·민감정보 재노출: RAG로 가져온 문서 일부가 사고 과정에 길게 복사되며 유출 위험이 커집니다.
  3. 규정 준수: 일부 산업은 “결정 근거”는 필요하지만 “모델의 내부 추론 텍스트”를 그대로 저장·노출하는 것을 꺼립니다.
  4. UX 비용: 긴 사고 과정은 사용자에게 피로감을 주고, 핵심 답을 가립니다.

핵심은 이겁니다.

  • 추론은 내부적으로 풍부하게 하되
  • 사용자에게는 최종 답검증 가능한 근거만 간결히 제공

SCR와 RAG를 결합하면 좋은 이유

RAG의 강점과 한계

RAG는 최신성·사내 지식·근거 제시에는 강합니다. 하지만 다음이 약점입니다.

  • 검색 결과가 애매하면 모델이 그럴듯하게 메우는 hallucination이 발생
  • 문서가 여러 개일 때 충돌 해결이 약함
  • 검색 쿼가 한 번 잘못되면 이후가 전부 틀어지는 query drift가 생김

SCR(Self-Consistency Reasoning)의 강점과 한계

SCR은 같은 문제를 여러 번 풀게 한 뒤, 다수결 또는 합의 규칙으로 답을 고르는 방식입니다.

  • 장점: 우연히 맞춘 한 번을 줄이고, 일관성을 높임
  • 단점: 비용이 늘고, 근거가 빈약하면 다수결로 틀린 답을 고를 수도 있음

결합 시 시너지

  • RAG로 근거 공간을 제한하면 SCR이 “같은 근거에서 여러 번 검증”하게 됩니다.
  • SCR로 검색 결과의 불확실성을 완화하고, 충돌 문서가 있을 때도 더 안정적으로 결론을 냅니다.

즉, RAG가 정보의 바닥을 만들고 SCR이 추론의 천장을 올립니다.

전체 아키텍처: 비노출 추론 파이프라인

권장 파이프라인은 아래와 같습니다.

  1. 입력 정규화 및 안전 필터링
  2. RAG 1차 검색
  3. 컨텍스트 압축 및 인용 단위 정리
  4. SCR 샘플링 k회 생성
  5. 합의(컨센서스) 선택 및 자기검증
  6. 최종 답변 생성 CoT 비노출
  7. 인용 및 감사 로그 저장(필요 시)

여기서 중요한 설계 포인트는 “추론 텍스트를 저장하지 않는다”가 아니라,

  • 저장하더라도 사용자에게는 노출하지 않으며
  • 운영 로그에는 최소한만 남기고
  • 근거는 문서 인용 중심으로 남긴다

입니다.

프롬프트 설계: CoT 없이도 강하게

다음 패턴을 추천합니다.

  • 모델에게는 내부적으로 충분히 생각하게 하되, 출력 형식에서 reasoning 필드를 금지
  • 출력은 answer, citations, assumptions, uncertainty 정도로 제한

예시 시스템 프롬프트입니다.

You are a helpful assistant.
Do not reveal chain-of-thought or internal reasoning.
Provide only:
- Final answer
- Citations (doc_id, section)
- Key assumptions (short)
- Uncertainty (short)
If you need reasoning, do it silently.

이 방식은 “생각하지 마라”가 아니라 “생각은 하되 밖으로 쓰지 마라”에 가깝습니다.

RAG 설계: SCR에 맞는 검색 컨텍스트 만들기

SCR을 붙이면 생성 횟수가 늘어납니다. 따라서 RAG는 더더욱 아래를 챙겨야 합니다.

1) 컨텍스트를 인용 단위로 쪼개기

문서를 통째로 넣으면 샘플마다 참조 지점이 달라져 합의가 어려워집니다.

  • chunk에 doc_id, section, offset 같은 메타데이터 부여
  • 인용은 chunk 단위를 기준으로 강제

2) 컨텍스트 압축

긴 컨텍스트는 비용을 키우고, 샘플 간 편차도 키웁니다.

  • 요약은 모델에 맡기되, 요약 결과에도 출처를 붙입니다.
  • 또는 키워드 기반으로 “필요한 문장만 추출”합니다.

3) 다중 쿼리 전략

한 번의 쿼리로 실패하는 경우가 많습니다.

  • 원 질문에서 핵심 엔티티, 제약 조건, 평가 기준을 분해한 뒤
  • 2~4개의 쿼리를 만들어 병렬 검색

SCR 설계: 합의 규칙이 성능을 좌우한다

SCR은 단순히 k번 생성해서 다수결하면 끝이 아닙니다. “무엇을 기준으로 합의할지”가 핵심입니다.

1) 샘플링 파라미터

  • temperature는 너무 낮으면 샘플 다양성이 줄어 SCR 효과가 약해집니다.
  • 너무 높으면 근거에서 벗어난 답이 늘어납니다.

실무적으로는 temperature를 중간값으로 두고, k를 3~7 정도에서 시작해 A/B로 맞춥니다.

2) 합의 기준을 구조화하기

자유 텍스트 다수결은 위험합니다. 대신 답을 구조화해서 비교 가능하게 만듭니다.

예를 들어 “정책 적용 여부”를 묻는 질문이면 결과를 이렇게 강제합니다.

{
  "decision": "allow | deny | unknown",
  "conditions": ["..."],
  "citations": [{"doc_id": "...", "section": "..."}]
}

그 다음 합의는 다음 규칙으로 합니다.

  • decision이 같은 샘플끼리 묶고 최빈값 선택
  • 동률이면 citations가 더 강한(더 많이, 더 직접적인) 쪽 우선
  • 그래도 동률이면 unknown으로 보수적 처리

3) 충돌 문서 처리

RAG에서 서로 충돌하는 문서를 가져오면 SCR은 오히려 “갈라지는 답”을 만들 수 있습니다.

이때는 합의 단계에서 “충돌 감지”를 먼저 합니다.

  • 동일 질문에 대해 상반된 decision이 2개 이상이면
  • 최종 답을 단정하지 않고 불확실성추가 확인 질문을 출력

구현 예시: TypeScript로 SCR·RAG 파이프라인

아래 코드는 개념 구현입니다. 실제로는 벡터DB, 리랭커, 모델 SDK에 맞게 교체하면 됩니다.

type Chunk = {
  docId: string;
  section: string;
  text: string;
};

type Sample = {
  decision: "allow" | "deny" | "unknown";
  conditions: string[];
  citations: { docId: string; section: string }[];
};

async function retrieve(query: string): Promise<Chunk[]> {
  // 1) 벡터 검색 + 키워드 검색 하이브리드
  // 2) 리랭킹
  return [];
}

function buildContext(chunks: Chunk[]): string {
  // 인용 단위를 명확히 하기 위해 doc/section 헤더를 붙여준다.
  return chunks
    .map(
      (c) =>
        `DOC: ${c.docId}\nSECTION: ${c.section}\nCONTENT: ${c.text}`
    )
    .join("\n\n");
}

async function generateSample(input: {
  question: string;
  context: string;
}): Promise<Sample> {
  // 모델 호출. 출력은 JSON만 허용.
  // CoT는 출력하지 말라고 명시.
  return {
    decision: "unknown",
    conditions: [],
    citations: [],
  };
}

function scoreSample(s: Sample): number {
  // 단순 예시: unknown 패널티, 인용 수 가산
  const base = s.decision === "unknown" ? -10 : 0;
  return base + s.citations.length;
}

function consensus(samples: Sample[]): Sample {
  const groups = new Map<string, Sample[]>();
  for (const s of samples) {
    const key = s.decision;
    groups.set(key, [...(groups.get(key) ?? []), s]);
  }

  // 최빈 decision 선택
  const sorted = [...groups.entries()].sort((a, b) => b[1].length - a[1].length);
  const top = sorted[0]?.[1] ?? [];

  // 동률 가능성 대비: 점수로 타이브레이크
  return top.sort((a, b) => scoreSample(b) - scoreSample(a))[0] ?? samples[0];
}

export async function answer(question: string) {
  const chunks = await retrieve(question);
  const context = buildContext(chunks);

  const k = 5;
  const samples: Sample[] = [];
  for (let i = 0; i < k; i++) {
    samples.push(await generateSample({ question, context }));
  }

  const best = consensus(samples);

  // 최종 응답은 간결하게. reasoning은 절대 포함하지 않는다.
  return {
    answer: best.decision,
    conditions: best.conditions,
    citations: best.citations,
    uncertainty: best.decision === "unknown" ? "Insufficient evidence in retrieved docs" : "",
  };
}

포인트는 2가지입니다.

  • 샘플 생성 단계에서 출력 스키마를 강제해 “비교 가능한 결과”를 만든다.
  • 최종 응답은 best만 사용하고, 샘플들의 중간 텍스트는 사용자에게 주지 않는다.

운영 관점 체크리스트: 비용·지연·안정성

SCR은 비용을 늘립니다. 그래서 운영에서는 아래를 반드시 같이 잡아야 합니다.

1) 동적 k 조절

질문 난이도에 따라 k를 고정하지 말고 조절합니다.

  • 검색 근거가 충분하고 일관되면 k=3
  • 충돌이 감지되면 k=7 또는 unknown 처리

2) 캐시 전략

  • RAG 검색 결과 캐시: query 정규화 후 캐시
  • 합의 결과 캐시: 정책 문서 버전이 같을 때만 재사용

3) 스트리밍과 리소스 누수

SCR은 병렬 호출을 유도하고, 스트리밍을 섞으면 토큰 중복이나 메모리 누수가 생기기 쉽습니다. LangChain 기반이라면 아래 글의 패턴이 그대로 도움이 됩니다.

4) 감사 로그 설계

“왜 이런 답을 냈는지”를 남기고 싶다면, CoT 대신 다음을 저장하세요.

  • 사용한 doc_id, section
  • 최종 구조화 결과(JSON)
  • 충돌 여부, 불확실성 플래그
  • 모델/프롬프트 버전, 검색 인덱스 버전

이렇게 하면 재현성과 컴플라이언스를 함께 챙기면서도, 내부 추론 텍스트 유출을 줄일 수 있습니다.

고급 패턴: SCR을 RAG 쿼리 단계에까지 확장

한 단계 더 나가면 “답변 생성”뿐 아니라 “검색 쿼리 생성”에도 SCR을 적용할 수 있습니다.

  • 쿼리 후보를 3개 만든 뒤
  • 각 쿼리로 검색한 결과를 비교하고
  • 가장 근거가 풍부한 컨텍스트를 선택

이 패턴은 질문이 모호하거나, 도메인 용어가 많을수록 효과가 큽니다.

실패 모드와 대응

1) 다수결로 틀린 답이 굳어지는 경우

  • 원인: RAG 근거가 편향되었거나, 중요한 문서가 누락됨
  • 대응: 하이브리드 검색, 리랭커 강화, unknown 우선 정책

2) 문서 충돌로 결론이 계속 갈리는 경우

  • 원인: 정책 버전 혼재, 문서 갱신 지연
  • 대응: 문서에 effective_date, version 메타데이터를 넣고 최신 우선

3) 지연 시간이 과도한 경우

  • 원인: k 고정, 컨텍스트 과대, 병렬 호출 제한
  • 대응: 동적 k, 컨텍스트 압축, 배치 호출, 캐시

정리

CoT 비노출은 단순한 출력 제약이 아니라 제품 품질과 보안을 동시에 잡는 설계 선택입니다. 여기에

  • RAG로 근거를 좁히고
  • SCR로 답의 일관성과 견고함을 올리며
  • 최종 출력은 구조화된 결론과 인용만 제공

하면, “추론은 강하지만 내부 사고는 드러나지 않는” 실무형 LLM 시스템을 만들 수 있습니다.

다음 단계로는 unknown을 적극적으로 쓰는 정책, 충돌 감지 규칙, 그리고 쿼리 생성 단계까지 포함한 멀티스테이지 SCR을 A/B 테스트로 다듬어 보세요.