Published on

CoT 노출 없이 정확도↑ - 자기검증 프롬프트 5패턴

Authors

서빙 환경에서 LLM 답변 정확도를 올리고 싶어지면 가장 먼저 떠오르는 게 CoT(Chain-of-Thought) 유도입니다. 하지만 제품에 그대로 노출하면 두 가지 문제가 생깁니다.

  • 정책 및 보안 이슈: 내부 추론 과정이 과도하게 노출되어 프롬프트 인젝션, 모델 탈출, 민감 정보 누출 리스크가 커집니다.
  • UX 및 비용 이슈: 장황한 추론 텍스트로 토큰이 늘고 지연이 증가합니다.

그래서 실무에서는 추론은 내부적으로 하되, 사용자에게는 요약된 근거/검증 결과만 내보내는 방식이 필요합니다. 이 글은 그 목적에 맞춘 자기검증(self-verification) 프롬프트 5패턴을 제공합니다. 각 패턴은 출력 포맷을 강제하고, 검증을 분리하며, 실패 시 재시도 할 수 있게 설계합니다.

또한 RAG를 쓴다면 “근거 문서가 없는데도 그럴듯하게 답하는” 현상이 자주 나오는데, 이때는 검색 품질부터 의심해야 합니다. 관련해서는 LangChain RAG에서 No relevant docs 7가지 원인도 함께 보면 좋습니다.

전제: CoT를 노출하지 않으면서 검증을 시키는 핵심

핵심은 간단합니다.

  1. 모델에게는 내부 추론을 허용하되
  2. 응답은 구조화된 짧은 결과로 제한하고
  3. 검증은 체크리스트/테스트/규칙 기반으로 표현하게 하며
  4. 실패 시 재생성 루프로 품질을 올립니다.

즉, “생각을 보여줘”가 아니라 “검증 가능한 결과를 내고, 스스로 검증 보고서를 짧게 제출해”로 바꾸는 겁니다.

아래 패턴들은 공통적으로 JSON 스키마 혹은 고정된 섹션을 사용합니다. MDX 환경에서는 부등호가 빌드 에러를 낼 수 있으니, 코드/기호는 반드시 코드 블록 또는 인라인 코드로 감쌌습니다.


패턴 1) 최종답 + 검증 체크리스트(요약형)

가장 범용적인 패턴입니다. 모델이 답을 만든 뒤, 스스로 오류 가능성이 큰 항목을 체크리스트로 점검하게 합니다. 이때 체크리스트는 “추론 과정”이 아니라 “검증 결과”만 포함하게 제한합니다.

프롬프트 템플릿

역할: 당신은 정확한 답변을 내는 전문가다.

요구사항:
- 내부적으로 충분히 검토하되, 중간 추론은 노출하지 마라.
- 아래 JSON 형식으로만 출력하라.
- verification.checks에는 검증 항목과 통과 여부, 짧은 근거만 적어라.
- 불확실하면 uncertainty를 true로 설정하고, 추가로 필요한 정보 질문을 questions에 적어라.

출력 JSON 스키마:
{
  "answer": "...",
  "verification": {
    "checks": [
      {"item": "...", "pass": true, "note": "..."}
    ],
    "uncertainty": false,
    "questions": ["..."]
  }
}

사용자 질문:
{question}

언제 쓰나

  • 고객지원, 운영 가이드, 기술 QnA처럼 정답이 하나가 아닐 수 있지만 틀리면 위험한 영역
  • “정확도”를 프로세스적으로 보장하고 싶을 때

구현 예시(Node.js)

import OpenAI from "openai";

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export async function answerWithChecklist(question) {
  const prompt = `역할: 당신은 정확한 답변을 내는 전문가다.

요구사항:
- 내부적으로 충분히 검토하되, 중간 추론은 노출하지 마라.
- 아래 JSON 형식으로만 출력하라.
- verification.checks에는 검증 항목과 통과 여부, 짧은 근거만 적어라.
- 불확실하면 uncertainty를 true로 설정하고, 추가로 필요한 정보 질문을 questions에 적어라.

출력 JSON 스키마:
{
  "answer": "...",
  "verification": {
    "checks": [
      {"item": "...", "pass": true, "note": "..."}
    ],
    "uncertainty": false,
    "questions": ["..."]
  }
}

사용자 질문:
${question}`;

  const res = await client.chat.completions.create({
    model: "gpt-4.1-mini",
    messages: [{ role: "user", content: prompt }],
    temperature: 0.2
  });

  return JSON.parse(res.choices[0].message.content);
}

운영 팁은 verification.checks를 로깅해두면, 나중에 “어떤 유형의 오류가 반복되는지”를 데이터로 볼 수 있습니다.


패턴 2) 2단계 분리: 생성 단계와 검증 단계 분리(리뷰어 모델)

한 번에 “답+검증”을 시키면, 모델이 자기 답을 합리화하는 경향이 있습니다. 그래서 작성자검토자를 분리하는 게 효과적입니다. 같은 모델을 써도 메시지 역할을 분리하면 품질이 올라가고, 가능하면 서로 다른 모델로 분리하면 더 좋아집니다.

흐름

  1. Draft 생성: 답만 생성
  2. Review 검증: draft를 입력으로 받아 오류/누락/모호함을 평가
  3. Fix 반영: 리뷰 결과를 기반으로 재작성

프롬프트 템플릿

[Draft 단계]
- 최종 답만 작성하라.
- 중간 추론은 쓰지 마라.

[Review 단계]
- 아래 draft를 검토하라.
- 오류 가능성, 누락, 전제 조건, 안전 이슈를 찾아라.
- JSON으로만 출력하라.

출력 JSON:
{
  "verdict": "pass" | "fail",
  "issues": [
    {"type": "factual" | "logic" | "missing" | "safety", "detail": "...", "severity": "low" | "med" | "high"}
  ],
  "fix_instructions": ["..."]
}

언제 쓰나

  • 배포된 지식봇, 사내 문서 자동화처럼 검수 프로세스가 필요한 경우
  • RAG 답변에서 “근거가 빈약한데 확신하는 톤”을 잡고 싶을 때

RAG의 경우 리뷰어에게 근거 인용이 실제로 답을 지지하는지를 체크하게 하세요. 근거 품질 이슈는 검색 단계 문제일 수도 있으니, 앞서 언급한 LangChain RAG에서 No relevant docs 7가지 원인에서 원인별로 점검하면 재발을 줄일 수 있습니다.


패턴 3) 반례 생성 후 방어: Counterexample 기반 자기검증

정확도가 흔들리는 답변은 보통 경계 조건에서 깨집니다. 그래서 모델에게 “이 답을 깨뜨릴 수 있는 반례를 3개 만들고, 그 반례를 방어할 수 있도록 답을 보강하라”를 시키면 강해집니다.

중요한 점은 반례를 길게 설명하게 하지 말고, 짧은 시나리오보강된 최종 답만 내게 하는 것입니다.

프롬프트 템플릿

너는 답변 품질을 강화하는 편집자다.

지시:
1) 사용자 질문에 대한 1차 답변을 만든다.
2) 그 답변이 틀릴 수 있는 반례/경계조건 3가지를 한 줄씩 만든다.
3) 반례를 반영해 최종 답변을 보강한다.

규칙:
- 중간 추론은 노출하지 마라.
- 아래 JSON으로만 출력하라.

출력 JSON:
{
  "draft": "...",
  "counterexamples": ["...", "...", "..."],
  "final": "..."
}

질문:
{question}

언제 쓰나

  • “대부분 맞는데 가끔 크게 틀리는” 유형의 답변
  • 운영/장애 대응, 보안 설정, 데이터 마이그레이션처럼 예외 케이스가 중요한 주제

예를 들어 DB 운영 글에서 VACUUM이 안 끝나는 이유는 bloat, wraparound, lock, IO 병목 등 반례가 다양합니다. 이런 류의 글을 생성할 때도 반례 검증이 유효합니다. 관련 사례는 PostgreSQL VACUUM 안끝남 - bloat·wraparound 해결처럼 원인 분기가 많아 “한 가지로 단정”하면 틀리기 쉬운 영역입니다.


패턴 4) 제약 기반 검증: 규칙을 먼저 선언하고 규칙 위반을 검사

LLM은 “그럴듯함”을 우선하기 때문에, 제약(Constraints)을 명시적으로 주고 위반 여부를 검사하게 하면 품질이 안정됩니다. 특히 코드/설정/절차형 답변에서 효과가 큽니다.

예시: 운영 절차 답변에 제약을 건다

  • 단계 수는 7단계 이하
  • 각 단계는 명령형 문장으로 시작
  • 위험 작업은 롤백을 포함
  • 불확실하면 추측하지 말고 질문

프롬프트 템플릿

역할: SRE 문서 작성자

제약:
- steps는 최대 7개
- 각 step은 동사로 시작
- 위험 작업에는 rollback을 포함
- 불확실한 내용은 추측 금지, questions에 질문 추가

출력 JSON:
{
  "steps": [
    {"title": "...", "detail": "...", "rollback": "..."}
  ],
  "constraint_report": [
    {"constraint": "...", "ok": true, "note": "..."}
  ],
  "questions": ["..."]
}

주제:
{topic}

언제 쓰나

  • 체크리스트/런북/장애 대응처럼 형식이 곧 품질인 문서
  • 팀 내 표준을 강제하고 싶을 때

이 패턴은 쿠버네티스 운영에서도 유용합니다. 예를 들어 이미지 풀 실패 대응은 단계가 길어지기 쉬운데, 제약을 걸면 과잉 서술을 줄이면서도 재현 가능한 절차를 만들기 좋습니다. 비슷한 운영 흐름은 EKS Pod ImagePullBackOff - ECR 인증 7단계처럼 “단계형”으로 정리될 때 효과가 큽니다.


패턴 5) 스코어링 + 재시도: Self-critique 점수로 자동 품질 게이트 만들기

실무에서 제일 강력한 건 “좋은 프롬프트 한 방”이 아니라, 품질 게이트입니다. 모델에게 답변을 만들게 한 뒤, 루브릭으로 점수화하고 기준 미달이면 재시도하는 방식입니다.

여기서도 CoT를 요구할 필요가 없습니다. 점수와 근거는 짧은 문장으로만 받으면 됩니다.

루브릭 예시

  • 정확성: 0~5
  • 완결성: 0~5
  • 모호성: 0~5(낮을수록 좋음)
  • 실행가능성: 0~5

프롬프트 템플릿

너는 답변을 채점하는 평가자다.

규칙:
- 중간 추론을 노출하지 마라.
- 아래 JSON으로만 출력하라.

루브릭:
- accuracy: 0~5
- completeness: 0~5
- ambiguity: 0~5
- actionability: 0~5

출력 JSON:
{
  "scores": {"accuracy": 0, "completeness": 0, "ambiguity": 0, "actionability": 0},
  "reasons": ["..."],
  "must_fix": ["..."]
}

평가 대상 답변:
{answer}

재시도 루프 예시(Python)

import json

def should_retry(scores: dict) -> bool:
    return (
        scores["accuracy"] < 4
        or scores["completeness"] < 4
        or scores["actionability"] < 4
        or scores["ambiguity"] > 2
    )

def improve_with_rubric(llm_generate, llm_grade, question: str, max_iter: int = 3):
    answer = llm_generate(question)

    for _ in range(max_iter):
        grade = json.loads(llm_grade(answer))
        if not should_retry(grade["scores"]):
            return {
                "answer": answer,
                "grade": grade,
                "iterations": _ + 1,
            }

        must_fix = "\n".join(f"- {x}" for x in grade.get("must_fix", []))
        answer = llm_generate(
            question + "\n\n수정 지시사항:\n" + must_fix + "\n\n중간 추론은 노출하지 말고, 최종 답만 개선해라."
        )

    return {"answer": answer, "grade": grade, "iterations": max_iter}

언제 쓰나

  • 제품 기능으로 LLM을 붙였고, 일정 수준 이하 답변을 차단해야 할 때
  • 동일 질문이 반복되는 상담/헬프데스크에서 일관된 품질이 필요할 때

운영에서 자주 터지는 함정 6가지

1) “CoT를 숨겨라”만으로는 부족하다

모델이 내부적으로 추론을 하더라도, 출력이 구조화되지 않으면 결국 장황해지거나 핵심이 흐려집니다. 출력 스키마가 필수입니다.

2) 검증 항목이 추상적이면 의미가 없다

체크리스트가 “논리적으로 타당함” 같은 문장으로 끝나면 검증이 아닙니다. “전제 조건을 명시했는가”, “버전/환경 의존성이 있는가”처럼 관찰 가능한 항목으로 만드세요.

3) 리뷰어가 원문을 따라간다

동일 모델, 동일 컨텍스트에서 리뷰하면 자기합리화가 생깁니다. 최소한 리뷰 단계에선 시스템 메시지로 역할을 강하게 분리하고, 가능하면 다른 모델이나 다른 샘플링 설정을 쓰세요.

4) RAG는 검증 이전에 검색 품질이 핵심이다

근거가 없는데 검증만 시키면 “근거 없음”을 반복할 뿐입니다. 검색 쿼리, 임베딩, chunking, top-k, 필터를 먼저 점검하세요. LangChain RAG에서 No relevant docs 7가지 원인에 체크리스트가 잘 정리돼 있습니다.

5) 재시도는 비용 폭탄이 될 수 있다

루브릭 재시도는 강력하지만 토큰 비용과 지연이 늘어납니다. max_iter를 2~3으로 제한하고, 특정 점수에서만 재시도하도록 게이트를 촘촘히 설계하세요.

6) “정답”이 아니라 “안전한 실패”가 목표인 경우가 많다

불확실할 때는 추측하지 말고 질문하게 만드는 것이 장기적으로 정확도를 올립니다. 패턴 1, 4에서 questions 필드를 둔 이유가 이것입니다.


실전 조합 레시피(추천)

  • 일반 QnA: 패턴 1(체크리스트) 단독
  • 고위험 운영/보안: 패턴 2(리뷰어 분리) + 패턴 4(제약) 조합
  • 경계조건이 많은 기술 문서: 패턴 3(반례) + 패턴 1(체크리스트)
  • 제품 품질 게이트: 패턴 5(스코어링) + 필요 시 패턴 2(리뷰어)

이렇게 조합하면 CoT를 사용자에게 노출하지 않으면서도, 모델이 스스로 오류를 줄이는 방향으로 유도할 수 있습니다.


마무리: “추론 노출” 대신 “검증 가능한 출력”으로 설계하자

CoT는 정확도를 올리는 데 도움이 되지만, 제품에서는 노출 비용이 큽니다. 대신 다음을 목표로 잡으면 됩니다.

  • 출력은 짧고 구조화
  • 검증은 체크리스트/루브릭/제약으로 가시화
  • 실패는 질문 또는 재시도로 안전하게 처리

위 5패턴을 템플릿화해두면, 팀 내에서 프롬프트 품질을 개인 역량이 아니라 프로세스로 끌어올릴 수 있습니다.