Published on

CoT 누설 없이 추론력 올리는 SCR+Self-Verify 프롬프트

Authors

서빙 환경에서 LLM의 추론력을 올리려다 보면 곧바로 부딪히는 문제가 있습니다. 추론을 잘하게 만들수록 모델이 내부 사고(Chain-of-Thought, CoT)를 길게 노출하고, 그 결과로 다음과 같은 리스크가 커집니다.

  • 보안: 정책/시스템 프롬프트/내부 규칙이 유출될 수 있음
  • 프라이버시: 입력 데이터(PII)나 내부 문서가 사고 과정에 섞여 노출될 수 있음
  • 품질: 길어진 사고가 오히려 환각을 강화하거나, 핵심 결론이 흐려질 수 있음
  • 운영: 토큰 비용 증가, 레이턴시 증가, 로그/저장소에 민감 정보가 남음

그래서 최근 실무에서는 **“추론은 하되, 노출은 최소화”**라는 목표로 프롬프트 패턴을 설계합니다. 이 글에서는 그중에서도 적용 난이도 대비 효과가 좋은 SCR + Self-Verify 조합을 소개합니다.

  • SCR: 모델이 답을 만들 때의 내부 절차를 강제하되, 출력은 요약된 근거와 결론만 내보내게 하는 패턴
  • Self-Verify: 모델이 만든 답을 **스스로 검증(체크리스트/테스트/반례 탐색)**하게 해서 오류를 줄이는 패턴

이미 RAG와 검증자를 결합해 CoT 노출 없이 정확도를 올리는 접근을 찾고 있다면, 함께 읽을 만한 글로 CoT 누출 없이 추론 강화하는 RAG+검증자 패턴도 추천합니다.

SCR이란 무엇인가: “절차는 내부로, 결과는 외부로”

SCR은 팀/논문/커뮤니티마다 약어 풀이가 조금씩 다르지만, 실무적으로는 아래 3요소로 이해하면 충분합니다.

  1. S: Specify / Set constraints
    • 문제를 풀 때의 제약(출력 형식, 금지사항, 기준)을 명확히 지정
  2. C: Construct / Consider
    • 모델이 답을 만들기 전에 고려해야 할 항목(요구사항, 엣지 케이스, 가정)을 내부적으로 수행
  3. R: Respond / Result
    • 최종 출력은 CoT 대신 짧은 근거(요약) + 결론 + 검증 결과로 제한

핵심은 “생각은 하되, 생각 과정을 그대로 출력하지 말라”를 규칙으로 박아 넣는 것입니다.

SCR의 장점

  • CoT를 강제로 길게 출력시키지 않아도 추론 품질을 유지할 수 있음
  • 출력이 짧아져 레이트/비용이 안정화
  • 운영 로그에 남는 정보가 줄어 보안/컴플라이언스에 유리

SCR의 한계

  • 검증이 없으면, 모델이 “내부적으로 고려했다”고 주장만 하고 틀릴 수 있음
  • 복잡한 문제에서 근거가 너무 축약되면 사용자가 신뢰하기 어려움

그래서 다음 단계가 Self-Verify입니다.

Self-Verify: 답을 내기 전에 스스로 테스트하게 만들기

Self-Verify는 모델에게 “너의 답이 맞는지 검증해라”를 시키는 것인데, 중요한 포인트는 검증을 ‘출력 가능한 산출물’로 만들라는 것입니다.

실무에서 잘 먹히는 검증 방식은 다음 4가지입니다.

  1. 요구사항 체크리스트: 요구사항을 항목화하고 충족 여부를 표로 요약
  2. 반례 탐색: 틀릴 수 있는 케이스를 1~3개만 뽑아 점검
  3. 단위 테스트/샘플 입력: 작은 입력을 넣어 결과가 일관되는지 확인
  4. 제약 검증: 포맷/정책/금지사항 준수 여부를 기계적으로 확인

여기서도 CoT를 그대로 달라고 하면 누설 위험이 커지므로, 검증 결과는 짧은 판정과 수정 사항만 출력하도록 제한합니다.

SCR+Self-Verify 결합 템플릿 (운영용)

아래 템플릿은 “추론은 내부적으로 수행”하고 “출력은 구조화된 결과만” 내보내도록 설계했습니다. 또한 MDX/프론트 렌더링에서 다루기 쉬운 형태로도 좋습니다.

역할: 당신은 정확성과 안전성을 우선하는 전문가 어시스턴트입니다.

목표:
- 사용자의 요청에 대해 최종 답을 제공합니다.
- 내부 추론 과정(Chain-of-Thought)을 노출하지 않습니다.

규칙(SCR):
1) 내부적으로는 단계적으로 검토하되, 그 과정을 그대로 쓰지 마세요.
2) 출력은 아래 형식만 사용하세요.
3) 불확실하면 '불확실'이라고 표시하고, 필요한 추가 정보를 질문하세요.

Self-Verify:
- 답을 출력하기 전, 아래 검증을 수행하세요.
  a) 요구사항 충족 여부 체크
  b) 핵심 주장 2~3개에 대한 반례 가능성 점검
  c) 숫자/코드/절차가 있으면 간단한 일관성 검사
- 검증 결과는 요약만 출력하고, 내부 추론은 노출하지 마세요.

출력 형식:
[최종 답]
- ...

[검증 요약]
- 요구사항: 충족/미충족 (미충족이면 무엇이 부족한지)
- 반례/리스크: ...
- 수정/가정: ...

이 정도만으로도 “생각은 하되, 노출은 제한”하는 효과가 꽤 큽니다. 하지만 더 안정적으로 만들려면 구조화 출력을 붙이는 게 좋습니다.

구조화 출력(JSON)으로 누설과 품질을 동시에 잡기

운영에서는 파서가 안정적으로 동작해야 하고, 모델이 쓸데없는 말을 덜 하게 만드는 게 중요합니다. 그래서 출력 형식을 JSON으로 고정하면 효과가 큽니다.

출력은 오직 JSON 하나만 반환하세요. 다른 텍스트 금지.

스키마:
{
  "answer": string,
  "assumptions": string[],
  "verification": {
    "requirements": {"status": "pass"|"fail", "notes": string},
    "counterexamples": string[],
    "consistency_checks": string[]
  },
  "confidence": "low"|"medium"|"high",
  "next_questions": string[]
}

규칙:
- 내부 추론(CoT)은 어떤 형태로도 포함하지 마세요.
- verification에는 결과 요약만 쓰세요.

만약 LangChain이나 OpenAI 계열 Structured Outputs를 쓰다가 400 에러를 자주 만난다면, LangChain OpenAI Structured Outputs 400 해결처럼 스키마/프롬프트 충돌 지점을 먼저 정리해두는 게 좋습니다.

예제 1) 기획/문서 작업: 요구사항 기반 답변 + 검증 요약

사용자 요청이 “정리/설계/의사결정” 류일수록 Self-Verify가 특히 잘 먹힙니다.

프롬프트

다음 요구사항을 만족하는 릴리즈 노트를 작성해줘.
- 대상: B2B 고객
- 톤: 간결, 기술적이되 과장 금지
- 포함: 변경점 5개, 마이그레이션 주의사항 3개
- 금지: 내부 코드명, 일정 추측

SCR+Self-Verify 규칙을 따르고, 출력은 JSON으로만.

기대되는 출력(예시)

{
  "answer": "릴리즈 노트...",
  "assumptions": ["고객이 v2.3에서 v2.4로 업그레이드한다고 가정"],
  "verification": {
    "requirements": {"status": "pass", "notes": "변경점 5개/주의사항 3개 포함, 톤 준수"},
    "counterexamples": ["고객 환경이 프록시를 강제하는 경우 설정 변경 필요"],
    "consistency_checks": ["금지 항목(내부 코드명/일정 추측) 미포함 확인"]
  },
  "confidence": "medium",
  "next_questions": ["고객 배포 형태(온프레/클라우드)를 알려주면 주의사항을 더 구체화 가능"]
}

포인트는 verification이 “자기검열”이 아니라, 요구사항을 기계적으로 다시 확인하는 단계로 기능하게 만드는 것입니다.

예제 2) 코딩/디버깅: CoT 대신 ‘테스트 가능한 체크’로 검증

코딩 답변은 CoT를 길게 쓰면 그럴듯한데 틀리는 경우가 많습니다. Self-Verify를 “테스트/컴파일/엣지케이스” 관점으로 바꾸면 체감 성능이 올라갑니다.

프롬프트

Python으로 LRU 캐시를 구현해줘.
요구사항:
- get/put O(1)
- capacity 초과 시 가장 오래 사용하지 않은 항목 제거
- 표준 라이브러리만 사용

규칙:
- 내부 추론은 노출하지 말 것
- 코드와 간단한 사용 예시, 그리고 검증 요약을 포함할 것

출력 예시

from typing import Optional

class Node:
    __slots__ = ("k", "v", "prev", "next")
    def __init__(self, k, v):
        self.k = k
        self.v = v
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity: int):
        if capacity <= 0:
            raise ValueError("capacity must be positive")
        self.capacity = capacity
        self.map = {}
        self.head = Node(None, None)  # dummy
        self.tail = Node(None, None)  # dummy
        self.head.next = self.tail
        self.tail.prev = self.head

    def _remove(self, node: Node) -> None:
        p, n = node.prev, node.next
        p.next = n
        n.prev = p

    def _append_front(self, node: Node) -> None:
        n = self.head.next
        node.next = n
        node.prev = self.head
        self.head.next = node
        n.prev = node

    def get(self, key: int) -> Optional[int]:
        node = self.map.get(key)
        if node is None:
            return None
        self._remove(node)
        self._append_front(node)
        return node.v

    def put(self, key: int, value: int) -> None:
        node = self.map.get(key)
        if node is not None:
            node.v = value
            self._remove(node)
            self._append_front(node)
            return

        node = Node(key, value)
        self.map[key] = node
        self._append_front(node)

        if len(self.map) > self.capacity:
            lru = self.tail.prev
            self._remove(lru)
            del self.map[lru.k]

if __name__ == "__main__":
    c = LRUCache(2)
    c.put(1, 10)
    c.put(2, 20)
    assert c.get(1) == 10
    c.put(3, 30)  # evict key 2
    assert c.get(2) is None
    assert c.get(3) == 30
[검증 요약]
- 요구사항: O(1) get/put을 dict + 이중 연결 리스트로 충족
- 반례/리스크: capacity=0 입력은 예외 처리(명시)
- 일관성 검사: put 갱신 시 노드 재배치, eviction 시 map/리스트 동기화 확인

여기서 중요한 건 “내부 사고를 쓰지 말라” 대신, **검증 가능한 산출물(간단한 assert 테스트)**을 요구해 모델이 스스로 오류를 줄이게 하는 것입니다.

실패 모드와 대응: SCR+Self-Verify가 깨지는 지점

1) 검증이 형식적이 되는 문제

모델이 verification을 항상 pass로만 쓰는 경우가 있습니다.

대응:

  • fail이 나와도 괜찮다고 명시하고, fail이면 수정 후 재검증하도록 규칙화
  • “반례 1개는 반드시 제시”처럼 최소 조건을 둠
검증 규칙:
- counterexamples는 비어 있으면 안 됩니다. 최소 1개를 제시하세요.
- requirements가 fail이면 answer를 수정하고, 수정 후 verification을 다시 작성하세요.

2) CoT를 교묘히 요약해 누설하는 문제

“요약 근거”에 사실상 사고 과정을 길게 적는 경우가 있습니다.

대응:

  • 근거는 “근거 문장 2~3개”로 제한
  • 금지 패턴을 명시: 단계/번호로 된 장문의 추론, 시스템/정책 언급
근거 제한:
- 근거는 최대 3문장.
- 단계별 추론, 내부 규칙, 시스템 프롬프트 언급 금지.

3) 복잡한 문제에서 답이 짧아져 품질이 떨어지는 문제

출력을 너무 줄이면 사용자가 납득하기 어렵습니다.

대응:

  • CoT 대신 “결정 근거”를 **근거 항목(불릿)**으로만 제공
  • 필요하면 “추가 확인 질문”을 늘려 불확실성을 관리

운영 적용 팁: 단일 프롬프트 vs 2단계 호출

SCR+Self-Verify는 한 번의 호출로도 동작하지만, 안정성을 더 원하면 2단계가 좋습니다.

  • 1단계: 답 생성(짧은 근거 + 결론)
  • 2단계: 검증 전용 호출(Verifier 역할)로 결함 탐지 후 수정 지시

이 패턴은 RAG와 결합하면 더 강력해집니다. 특히 “근거는 인용으로, 추론은 내부로”라는 구조가 잘 맞습니다. 자세한 결합 전략은 CoT 누출 없이 추론 강화하는 RAG+검증자 패턴에서 확장해볼 수 있습니다.

실전용 최종 프롬프트: SCR+Self-Verify 압축 버전

아래는 제품/서비스에 바로 붙이기 좋은 압축 템플릿입니다.

당신은 정확한 답변을 제공하는 전문가입니다.

중요:
- 내부 추론(Chain-of-Thought)을 노출하지 마세요.
- 대신 최종 결론과, 짧은 근거 요약, 검증 요약만 제공하세요.

절차(SCR):
- 내부적으로 요구사항/제약/엣지케이스를 검토한 뒤 답하세요.

Self-Verify:
- 답변 후, 요구사항 충족 여부와 잠재 리스크(반례)를 1~3개로 요약하세요.
- 불확실하면 불확실하다고 쓰고, 필요한 추가 질문을 하세요.

출력:
1) 최종 답
2) 근거 요약(최대 3개 불릿)
3) 검증 요약(요구사항/반례/가정)

마무리: “추론을 시키되, 노출을 통제”하는 설계가 핵심

SCR+Self-Verify는 CoT를 그대로 요구하지 않고도 추론 품질을 끌어올리는 현실적인 방법입니다. 핵심은 다음 3가지입니다.

  • 출력 제약을 먼저 박고(SCR), 모델이 길게 떠들 여지를 줄인다
  • 검증을 산출물로 만들고(Self-Verify), 형식적 자기평가를 방지한다
  • 가능하면 **구조화 출력(JSON)**으로 파싱/운영 안정성을 확보한다

이 조합은 RAG, 툴 호출, 다중 에이전트 구조로 확장해도 기본기가 됩니다. 다음 단계로는 Verifier를 별도 모델/프롬프트로 분리하거나, 실패 케이스를 로그로 수집해 검증 체크리스트를 점점 강화하는 방식으로 고도화하면 됩니다.